Outro dia, eu estava ministrando um workshop de JavaScript e um dos participantes me fez uma pergunta sobre JS durante o intervalo do almoço que realmente me fez pensar. O senhor disse que se deparou com isso acidentalmente, mas estou um pouco cético; pode ter sido apenas um truque intencional do WTF!
De qualquer forma, entendi errado nas primeiras vezes em que tentei analisá-lo, e tive que executar o código por meio de um analisador e, em seguida, consulte a especificação (e um guru de JS!) para descobrir o que estava acontecendo. Como aprendi algumas coisas no processo, achei que poderia compartilhar com os senhores.
Não que eu espere que o senhor venha a escrever (ou ler, espero!) intencionalmente um código como esse, mas ser capaz de pensar mais como o JavaScript sempre ajuda o senhor a escrever um código melhor.
A configuração
A pergunta que me foi feita foi a seguinte: por que essa primeira linha “funciona” (compila/executa), mas a segunda linha dá um erro?
[[]][0]++; []++;
O raciocínio por trás desse enigma é que [[]][0]
deve ser o mesmo que []
portanto, ambos devem funcionar ou ambos devem falhar.
Minha primeira resposta, depois de pensar sobre isso por alguns instantes, foi que ambos deveriam falhar, mas por motivos diferentes. Eu estava incorreto em vários aspectos. De fato, a primeira é válida (mesmo que seja bastante boba).
Eu estava errado, apesar do fato de que eu estava tentando pensar como o JavaScript pensa. Infelizmente, meu raciocínio estava errado. Não importa o quanto o senhor o senhor saiba, o senhor ainda pode realizar facilmente o senhor não sabe , coisas.
É exatamente por isso que eu desafio as pessoas a admitir: “O senhor não conhece a JS”.; nenhum de nós jamais conseguiu sabe algo tão complexo quanto uma linguagem de programação como JS. Aprendemos algumas partes, depois aprendemos mais e continuamos aprendendo. É um processo eterno, não um destino.
Meus erros
Primeiro, vi os dois ++
e meu instinto foi que ambos falhariam porque o postfix unário ++
, como em x++
é, em sua maior parte, equivalente a x = x + 1
, o que significa que o x
(seja ele qual for) deve ser válido como algo que pode aparecer no lado esquerdo de um =
atribuição.
Na verdade, essa última parte é É verdade que eu estava certo sobre isso, mas pelos motivos errados.
O que eu pensava erroneamente era que o x++
é mais ou menos como o x = x + 1
; nesse pensamento, []++
sendo [] = [] + 1
seria inválido. Embora isso pareça estranho, na verdade não há problema algum. No ES6, o [] = ..
é uma desestruturação de matriz válida.
Pensando no x++
como x = x + 1
é um pensamento equivocado e preguiçoso, e eu não deveria estar surpreso por ele ter me desviado do caminho.
Além disso, eu também estava pensando na primeira linha de forma errada. O que eu pensei foi que o [[]]
está criando uma matriz (a parte externa do [ ]
) e, em seguida, a matriz interna []
interno está tentando ser um acesso à propriedade, o que significa que ele é transformado em string (para ""
), então é como se o [""]
. Isso é um absurdo. Não sei por que meu cérebro estava confuso aqui.
É claro que, para o exterior, o [ ]
seja uma matriz sendo acessado, ele precisaria ser como x[[]]
onde x
é a coisa que está sendo acessada, não apenas [[]]
por si só. Enfim, pensei errado. Que tolice a minha.
Pensamento Corrigido
Vamos começar com a correção mais fácil de pensar. Por que é []++
inválido?
Para obter a resposta real, devemos ir à fonte oficial de autoridade em tais tópicos, a especificação!
Na linguagem das especificações, o ++
em x++
é um tipo de “Expressão de atualização” chamou o “Postfix Increment Operator” (Operador de Incremento do Postfix). Ele requer o x
seja uma parte válida do “Left-Hand Side Expression” (Expressão do lado esquerdo) – em um sentido amplo, uma expressão que é válida no lado esquerdo de uma =
. Na verdade, a maneira mais precisa de pensar nisso é: não é o lado esquerdo de um =
mas sim, destino válido de uma atribuição.
Observando a lista de expressões válidas que podem ser alvos de uma atribuição, vemos coisas como “Expressão primária” e “Expressão de membro”, entre outros.
Se o senhor pesquisar Expressão primária, o senhor descobre que um “Array Literal” (como o nosso []
!) é válido, pelo menos do ponto de vista da sintaxe.
Então, espere! []
pode ser uma expressão do lado esquerdo e, portanto, é válida para aparecer ao lado de um ++
. Hmmm. Por que então o []++
dá um erro?
O que o senhor pode não perceber, e eu percebi, é que não se trata de um SyntaxError
de forma alguma! É um erro de tempo de execução chamado ReferenceError
.
De vez em quando, as pessoas me perguntam sobre outro resultado desconcertante – e totalmente relacionado! – em JS, que esse código é uma sintaxe válida (mas ainda assim falha no tempo de execução):
2 = 3;
Obviamente, um literal de número não deve ser algo que possamos atribuir ao senhor. Isso não faz sentido.
Mas não se trata de uma sintaxe inválida. É apenas uma lógica de tempo de execução inválida.
Então, qual parte da especificação faz com que o 2 = 3
falhe? A mesma razão pela qual o 2 = 3
falha é a mesma razão pela qual o []++
falha.
Ambas as operações usam um algoritmo abstrato na especificação chamado “PutValue”.. A etapa 3 desse algoritmo diz:
Se Type(V) não for Reference, lance uma exceção ReferenceError.
Reference
é um tipo de especificação especial que se refere a qualquer tipo de expressão que represente uma área na memória onde algum valor possa ser atribuído. Em outras palavras, para ser um alvo válido, o senhor precisa ser um Reference
.
Claramente, 2
e []
não são Reference
s, por isso, em tempo de execução, o senhor recebe um ReferenceError
; eles não são alvos de atribuição válidos.
Mas e quanto a…?
Não se preocupe, não esqueci a primeira linha do snippet, que funciona. Lembre-se de que meu raciocínio estava totalmente errado em relação a isso, portanto, tenho que fazer algumas correções.
[[]]
por si só não é um acesso a um array. É apenas um valor de array que contém outro valor de array como seu único conteúdo. Pense nisso da seguinte forma:
var a = []; var b = [a]; b; // [[]]
O senhor vê?
Então, agora, [[]][0]
, do que se trata? Mais uma vez, vamos detalhar isso com algumas variáveis temporárias.
var a = []; var b = [a]; var c = b[0]; c; // [] -- aka, `a`!
Portanto, a premissa da configuração original é correto. [[]][0]
é mais ou menos o mesmo que apenas []
em si.
De volta à pergunta original: por que então a linha 1 funciona e a linha 2 não?
Como observamos anteriormente, o “Expressão de atualização” requer um “LeftHandSideExpression” (expressão do lado esquerdo). Um dos tipos válidos dessas expressões é “Member Expression” (Expressão de membro), como [0]
em x[0]
– essa é uma expressão de membro!
O senhor está familiarizado? [[]][0]
é uma expressão de membro.
Portanto, estamos bem em termos de sintaxe. [[]][0]++
é válido.
Mas, espere! O senhor espere!
Se []
não for um Reference
, como poderia o [[]][0]
– o que resulta em apenas []
, lembre-se! – possivelmente seja considerado um Reference
para que o PutValue(..)
(descrito acima) não dê um erro?
É aqui que as coisas ficam um pouco complicadas. Uma dica para meu amigo Allen-Wirfs Brock, ex-editor da especificação JS, por me ajudar a ligar os pontos.
O resultado de uma expressão de membro não é o valor em si ([]
), mas sim um Reference
para esse valor – veja Etapa 8 aqui. Então, de fato, o [0]
está nos dando um referência para a 0ª posição dessa matriz externa, em vez de nos fornecer o valor real nessa posição.
E é por isso que é válido usar o [[]][0]
como uma expressão do lado esquerdo: na verdade, é um Reference
afinal de contas!
De fato, o ++
realmente atualização o valor, como podemos ver se capturarmos esses valores e os inspecionarmos posteriormente:
var a = [[]]; a[0]++; a; // [1]
O a[0]
nos dá a expressão do membro []
e a expressão ++
como uma expressão matemática a transformará em um número primitivo, que é o primeiro ""
e depois 0
. Os ++
incrementa esse valor para 1
e o atribui ao a[0]
. É como se o a[0]++
fosse de fato a[0] = a[0] + 1
.
Uma pequena observação: se o senhor executar o [[]][0]++
no console do seu navegador, ele informará 0
, não 1
ou [1]
. Por quê?
Porque ++
retorna o valor “original” (bem, depois da coerção, de qualquer forma – veja etapa 2 & 5 aqui), não o valor atualizado. Portanto, o 0
volta, e o 1
é colocado na matriz por meio desse Reference
.
É claro que, se o senhor não mantiver o array externo em uma variável, como fizemos, essa atualização é discutível, pois o valor em si desaparece. Mas isso foi atualizado, no entanto. Reference
. Legal!
Pós-corrigido
Não sei se o senhor aprecia JS ou se sente frustrado com todas essas nuances. Mas esse esforço me faz respeitar mais a linguagem e renova meu vigor para continuar aprendendo ainda mais. Eu acho que qualquer linguagem de programação terá seus cantos e recantos, alguns dos quais nos agradam e outros nos deixam loucos!
Independentemente de sua convicção, não deve haver discordância quanto ao fato de que, seja qual for a ferramenta escolhida, pensar mais como a ferramenta o torna melhor no uso dessa ferramenta. Feliz pensamento JavaScript!
Sobre Kyle Simpson
Kyle Simpson é um engenheiro de software orientado para a Web, amplamente aclamado por sua série de livros “You Don’t Know JS” e por quase 1 milhão de horas vistas de seus cursos on-line. O superpoder de Kyle é fazer perguntas melhores, e ele acredita profundamente no uso máximo das ferramentas minimamente necessárias para qualquer tarefa. Como um “tecnólogo centrado no ser humano”, ele é apaixonado por unir humanos e tecnologia, desenvolvendo organizações de engenharia para resolver os problemas certos, de maneiras mais simples. Kyle sempre lutará pelas pessoas por trás dos pixels.