este post é uma homenagem à fantástica palestra “Wat” de Gary Bernhardt, na qual ele aponta as peculiaridades de algumas construções de linguagem em Ruby e JavaScript. Se você ainda não assistiu a palestra, recomendo fortemente que você reserve um tempo e faça exatamente isso! É apenas cerca de 4 minutos de duração e altamente divertido, eu prometo.
em sua palestra, Gary mostra esses quatro fragmentos de código JavaScript:
vemos muitos colchetes, chaves e sinais de mais. Aqui está o que esses fragmentos avaliar a:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
Quando eu vi esses exemplos, pela primeira vez, eu pensei: “Uau, isso parece confuso!”Os resultados podem parecer inconsistentes ou mesmo arbitrários, mas tenham paciência comigo aqui. Todos esses exemplos são realmente muito consistentes e não tão ruins quanto parecem!
# fragmento #1: +
vamos começar com o primeiro fragmento:
+ // ""
como podemos ver, aplicar o operador +
a duas matrizes vazias resulta em uma string vazia. Isso ocorre porque a representação de string de uma matriz é a representação de string de todos os seus elementos, concatenada junto com vírgulas:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
uma matriz vazia não contém nenhum elemento, portanto, sua representação de string é uma string vazia. Portanto, a concatenação de duas strings vazias é apenas mais uma string vazia.
# fragmento #2: + {}
até agora, tão bom. Vamos agora examinar o segundo fragmento:
+ {}// ""
observe que, como não estamos lidando com dois números, o operador +
mais uma vez executa concatenação de string em vez de adicionar dois valores numéricos.
na seção anterior, já vimos que a representação de string de uma matriz vazia é uma string vazia. A representação de string do literal de objeto vazio aqui é o valor padrão ""
. Prepending uma string vazia não altera o valor, então ""
é o resultado final.
em JavaScript, os objetos podem implementar um método especial chamado toString()
que retorna uma representação de string personalizada do objeto no qual o método é chamado. Nosso literal de objeto vazio não implementa esse método, então estamos voltando à implementação padrão do protótipo Object
.
# fragmento #3: {} +
eu diria que até agora, os resultados não foram muito inesperados. Eles simplesmente seguiram as regras de coerção de tipo e representações de string padrão em JavaScript.
no entanto, {} +
é onde os desenvolvedores começam a ficar confusos:
{} + // 0
por que vemos 0
(o número zero) se digitamos a linha acima em um REPL JavaScript como o console do navegador? O resultado não deveria ser uma string, assim como + {}
era?
Antes de resolver o enigma, considere as três diferentes formas de o +
operador pode ser usado:
// 1) Addition of two numeric values2 + 2 == 4// 2) String concatenation of two values"2" + "2" == "22"// 3) Conversion of a value to a number+2 == 2+"2" == 2
Nos dois primeiros casos, o +
operador é um operador binário pois possui dois operandos (à esquerda e à direita). No terceiro caso, o operador +
é um operador unário porque possui apenas um único operando (à direita).
considere também os dois significados possíveis de {}
em JavaScript. Normalmente, escrevemos {}
para significar um literal de objeto vazio, mas se estivermos na posição de instrução, a gramática JavaScript especifica {}
para significar um bloco vazio. O seguinte pedaço de código define dois blocos vazios, nenhum dos quais é um literal de objeto:
{}// Empty block{ // Empty block}
Vamos dar uma olhada em nossa fragmento novamente:
{} +
Deixe-me mudar o espaço em branco um pouco para torná-lo mais claro como o motor de JavaScript vê o código:
{ // Empty block}+;
agora podemos ver claramente o que está acontecendo aqui. Temos uma instrução block seguida por outra instrução que contém uma expressão unary +
operando em uma matriz vazia. O ponto e vírgula à direita é inserido automaticamente de acordo com as regras do ASI (inserção automática de ponto e vírgula).
você pode verificar facilmente no console do navegador que +
avalia como 0
. A matriz vazia tem uma string vazia como sua representação de string, que por sua vez é convertida para o número zero pelo operador +
. Finalmente, o valor da última instrução (+
, neste caso) é relatado pelo console do navegador.
Alternativamente, você pode alimentar ambos os trechos de código para um analisador JavaScript, como Esprima e comparar as árvores de sintaxe abstratas resultantes. Aqui está a AST + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
E aqui está o AST {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
A confusão deriva de uma nuance do JavaScript gramática que usa chaves, tanto para literais de objeto e blocos. Na posição de instrução, uma cinta de abertura inicia um bloco, enquanto na posição de expressão uma cinta de abertura inicia um literal de objeto.
#Fragment #4: {} + {}
Finalmente, vamos, rapidamente, dê uma olhada no nosso último fragmento {} + {}
:
{} + {}// NaN
Bem, a adição de dois literais de objeto é, literalmente, “não é um número” — mas nós estamos adicionando dois literais de objeto aqui? Não deixe as chaves enganá-lo novamente! Isso é o que está acontecendo:
{ // Empty block}+{};
é praticamente o mesmo negócio que no exemplo anterior. No entanto, agora estamos aplicando o operador unary plus a um literal de objeto vazio. Isso é basicamente o mesmo que fazer Number({})
, o que resulta em NaN
porque nosso literal de objeto não pode ser convertido em um número.
se você deseja que o mecanismo JavaScript analise o código como dois literais de objeto vazios, envolva o primeiro (ou todo o código) entre parênteses. Agora você deve ver o resultado esperado:
({}) + {}// ""({} + {})// ""
o parêntese de abertura faz com que o analisador tente reconhecer uma expressão, e é por isso que ele não trata o {}
como um bloco (o que seria uma declaração).
#resumo
agora você deve ver por que os quatro fragmentos de código avaliam a maneira como eles fazem. Não é arbitrário ou aleatório; as regras de coerção de tipo são aplicadas exatamente como estabelecido na especificação e na gramática da linguagem.
lembre-se de que, se uma chave de abertura for o primeiro caractere a aparecer em uma instrução, ela será interpretada como o início de um bloco em vez de um literal de objeto.