A linguagem não tem. Em especificação do EcmaScript (o nome real da linguagem) não exige isto, portanto depende da implementação, tem que ver se o engine que está usando está faz ou não.
O nome JavaScript originalmente foi criado pela Netscape, depois herdado pela Mozilla, ainda que todos usam o nome dele como se fosse o oficial da linguagem.
A otimização chamada Tail Call Elimination deve estar presente em algum engine mais padrão (o que achei depois foi o Safari, nenhum outro, mas pode existir).
De qualquer forma essa otimização não existe para fazer o código ficar mais rápido, ela apenas impede que seja criada uma pilha enorme sendo que em cada chamada tem que reservar espaço para a variável, o que em alguns casos vai estourar o limite da pilha (o tal do stack overflow). Fazendo isso, colateralmente, a performance ficará melhor.
Se um executou muito rápido e o outro não, significa que deve ter alguma otimização, mas não em todas as situações. Eu nem garanto que essa seja a causa, pode ser até outra otimização.
O JITter poderia ter achado um meio de otimizar em um e não no outro, eu diria que é o mais provável. A resposta do kht dá detalhes que podem indicar isso. Ele fala em medir o tempo de carga de um e não do outro. Eu vou mais além, pode ter ocorrido até mesmo o tempo de warm-up do JITter. E é provável que as bibliotecas que ele usou sabe como lidar com isso e só medir o que importa, descartando esse tempo, e principalmente já descartar o tempo de carga do runtime e compilação. Portanto pode ter ocorrido umamedida errada.
Poderia ser que só faça a otimização quando faz a chamada simples da mesma função (segundo caso), mas quando a chamada está contida dentro de uma expressão (primeiro caso) não faça. No fim, vemos lá na resposta do kht que as chamas não são otimizadas nos dois casos.
No geral, se tiver uma otimização, ótimo, se não tiver, é ok. Só entenda que não pode contar com ela, ninguém é obrigado a garantir isso, e se não tiver e seu código estourar a pilha é problema seu, não é questão de performance, que pode ser um problema extra.
Ainda considerando que poderia ter ocorrido otimização, você experimentou com o V8 (pelo menos o usado nessa versão do Node) e viu esse resultado, mas pode mudar, otimização não é algo que deve se valer, a não ser que esteja em especificação e todos os engines que você usa estejam comprometidos a se conformar. Novamente, não foi o TCE que aconteceu.
De fato, até onde pesquisei, o V8 não faz essa eliminação, mas pode ter mudado recentemente para casos triviais.
Nada indica que uma empilha e a outra não, pelo menos de forma natural. E de fato na última edição do kht fica claro que ambas chamam igual.
É possível garantir a eliminação com uma técnica de trampolim, mas pra quê?
O melhor a fazer é usar um loop, ele é rápido e mais garantido. Só use recursão quando ela for primordial. Inclusive o loop também costuma ser melhor que fazer um mapeamento, mesmo que a moda seja usar o jeito "mais bonitinho".
- O que é um método recursivo?
- O que é uma recursão de cauda?
- Quando usar recursão e quando usar laços?
- Qual é a vantagem de usarmos funções recursivas?
Faz sentido para você?
Espero ter ajudado.
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente. Para saber quando, me segue nas suas plataformas preferidas. Quase não as uso, não terá infindas notificações (links aqui).