Qual é a explicação para esses comportamentos bizarros de JavaScript mencionados na palestra 'Wat' do CodeMash 2012?


753

A palestra 'Wat' para o CodeMash 2012 basicamente aponta algumas peculiaridades bizarras com Ruby e JavaScript.

Eu fiz um JSFiddle dos resultados em http://jsfiddle.net/fe479/9/ .

Os comportamentos específicos do JavaScript (como eu não conheço Ruby) estão listados abaixo.

Descobri no JSFiddle que alguns dos meus resultados não correspondiam aos do vídeo e não sei por que. No entanto, estou curioso para saber como o JavaScript está lidando com o trabalho nos bastidores em cada caso.

Empty Array + Empty Array
[] + []
result:
<Empty String>

Estou bastante curioso sobre o +operador quando usado com matrizes em JavaScript. Isso corresponde ao resultado do vídeo.

Empty Array + Object
[] + {}
result:
[Object]

Isso corresponde ao resultado do vídeo. O que está acontecendo aqui? Por que isso é um objeto? O que o +operador faz?

Object + Empty Array
{} + []
result:
[Object]

Isso não corresponde ao vídeo. O vídeo sugere que o resultado é 0, enquanto eu recebo [Objeto].

Object + Object
{} + {}
result:
[Object][Object]

Isso também não corresponde ao vídeo, e como a saída de uma variável resulta em dois objetos? Talvez meu JSFiddle esteja errado.

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

Fazendo wat + 1 resultados em wat1wat1wat1wat1...

Eu suspeito que este é apenas um comportamento direto que tentar subtrair um número de uma string resulta em NaN.


4
O {} + [] é basicamente o único complicado e dependente da implementação, como explico aqui , porque depende de ser analisado como uma declaração ou expressão. Em que ambiente você está testando (obtive o 0 esperado no Firefow e Chrome, mas obtive o "[objeto Objeto]" nos NodeJs)?
hugomg

1
Estou executando o Firefox 9.0.1 no Windows 7, e jsFiddle avalia-lo para [objeto]
NibblyPig

@missingno eu recebo 0 no NodeJS REPL
OrangeDog

41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson

1
@missingno Postou a pergunta aqui , mas para {} + {}.
Jonic Biză

Respostas:


1479

Aqui está uma lista de explicações para os resultados que você está vendo (e deveria estar vendo). As referências que estou usando são do padrão ECMA-262 .

  1. [] + []

    Ao usar o operador de adição, os operandos esquerdo e direito são convertidos primeiro em primitivos ( §11.6.1 ). Conforme §9.1 , a conversão de um objeto (neste caso, uma matriz) para uma primitiva retorna seu valor padrão, que para objetos com um toString()método válido é o resultado da chamada object.toString()( §8.12.8 ). Para matrizes, é o mesmo que chamar array.join()( §15.4.4.2 ). A união de uma matriz vazia resulta em uma cadeia vazia, portanto, a etapa 7 do operador de adição retorna a concatenação de duas cadeias vazias, que é a cadeia vazia.

  2. [] + {}

    Semelhante [] + [], ambos os operandos são convertidos em primitivos primeiro. Para "Objetos de objeto" (§15.2), esse é novamente o resultado da chamada object.toString(), que para objetos não nulos e indefinidos é "[object Object]"( §15.2.4.2 ).

  3. {} + []

    O {}aqui não é analisado como um objeto, mas como um bloco vazio ( §12.1 , pelo menos enquanto você não estiver forçando essa declaração a ser uma expressão, mas mais sobre isso posteriormente). O valor de retorno de blocos vazios está vazio, portanto, o resultado dessa instrução é o mesmo que +[]. O +operador unário ( §11.4.6 ) retorna ToNumber(ToPrimitive(operand)). Como já sabemos, ToPrimitive([])é a string vazia e, de acordo com §9.3.1 , ToNumber("")é 0.

  4. {} + {}

    Semelhante ao caso anterior, o primeiro {}é analisado como um bloco com valor de retorno vazio. Novamente, +{}é o mesmo que ToNumber(ToPrimitive({}))e ToPrimitive({})é "[object Object]"(consulte [] + {}). Então, para obter o resultado +{}, precisamos aplicar ToNumbera string "[object Object]". Ao seguir as etapas do §9.3.1 , obtemos NaNcomo resultado:

    Se a gramática não puder interpretar a String como uma expansão de StringNumericLiteral , o resultado de ToNumber será NaN .

  5. Array(16).join("wat" - 1)

    Conforme §15.4.1.1 e §15.4.2.2 , Array(16)cria uma nova matriz com o comprimento 16. Para obter o valor do argumento para ingressar, as seções # 11.6.2 # 5 e # 6 mostram que precisamos converter os dois operandos em um número usando ToNumber. ToNumber(1)é simplesmente 1 ( §9.3 ), enquanto que ToNumber("wat")novamente é NaNcomo §9.3.1 . O passo 7 de §11.6.2 , §11.6.3 determina que

    Se um dos operandos for NaN , o resultado será NaN .

    Então o argumento para Array(16).joiné NaN. Seguindo § 15.4.4.5 ( Array.prototype.join), temos que invocar ToStringo argumento, que é "NaN"( §9.8.1 ):

    Se m for NaN , retorne a String "NaN".

    Após o passo 10 de §15.4.4.5 , obtemos 15 repetições da concatenação "NaN"e da sequência vazia, que é igual ao resultado que você está vendo. Ao usar em "wat" + 1vez de "wat" - 1como argumento, o operador de adição converte 1em uma cadeia de caracteres em vez de converter "wat"em um número, portanto efetivamente chama Array(16).join("wat1").

Quanto ao motivo pelo qual você está vendo resultados diferentes para o {} + []caso: Ao usá-lo como argumento de função, você está forçando a instrução a ser uma ExpressionStatement , o que torna impossível analisar {}como bloco vazio; portanto, é analisado como um objeto vazio literal.


2
Então, por que [] +1 => "1" e [] -1 => -1?
Rob Elsner 26/10

4
O @RobElsner []+1segue basicamente a mesma lógica que []+[], apenas com o 1.toString()operando rhs. Para []-1ver a explicação do "wat"-1ponto 5. Lembre-se de que ToNumber(ToPrimitive([]))é 0 (ponto 3).
Ventero 26/10

4
Esta explicação está faltando / omite muitos detalhes. Por exemplo, "converter um objeto (neste caso, uma matriz) em uma primitiva retorna seu valor padrão, que para objetos com um método toString () válido é o resultado da chamada de object.toString ()" está completamente ausente desse valorOf of [] é chamado primeiro, mas como o valor de retorno não é um primitivo (é uma matriz), o toString de [] é usado. Eu recomendaria para olhar este em vez de verdade indepth explicação 2ality.com/2012/01/object-plus-object.html
jahav

30

Isso é mais um comentário do que uma resposta, mas, por algum motivo, não posso comentar sua pergunta. Eu queria corrigir o seu código JSFiddle. No entanto, eu postei isso no Hacker News e alguém sugeriu que eu o republicasse aqui.

O problema no código JSFiddle é que ({})(abrir chaves entre parênteses) não é o mesmo que {}(abrir chaves como o início de uma linha de código). Então, quando você digita, out({} + [])está forçando a {}ser algo que não é quando você digita {} + []. Isso faz parte do 'wat'-ness geral do Javascript.

A ideia básica era simples: o JavaScript queria permitir os dois formulários:

if (u)
    v;

if (x) {
    y;
    z;
}

Para isso, foram feitas duas interpretações da chave de abertura: 1. não é necessária e 2. pode aparecer em qualquer lugar .

Esta foi uma jogada errada. O código real não tem uma chave de abertura aparecendo no meio do nada, e o código real também tende a ser mais frágil quando usa a primeira forma e não a segunda. (Uma vez a cada dois meses no meu último emprego, eu era chamado para a mesa de um colega de trabalho quando as modificações no meu código não estavam funcionando, e o problema era que eles adicionaram uma linha ao "se" sem adicionar encaracolados Eu acabei de adotar o hábito de sempre usar chaves, mesmo quando você está escrevendo apenas uma linha.)

Felizmente, em muitos casos, o eval () replicará toda a watidez do JavaScript. O código JSFiddle deve ler:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[Também é a primeira vez que escrevo document.writeln em muitos e muitos anos, e me sinto um pouco suja escrevendo qualquer coisa que envolva document.writeln () e eval ().]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Eu discordo (tipo de): Eu tenho muitas vezes nos últimos blocos usados como este para variáveis de escopo em C . Esse hábito foi adquirido por um tempo atrás ao fazer C incorporado, onde variáveis ​​na pilha ocupam espaço; portanto, se não forem mais necessárias, queremos que o espaço seja liberado no final do bloco. No entanto, o ECMAScript apenas escopo dentro dos blocos function () {}. Portanto, embora eu discorde que o conceito está errado, concordo que a implementação em JS está ( possivelmente ) errada.
amigos estão dizendo

4
@JessTelford No ES6, você pode usar letpara declarar variáveis ​​com escopo de bloco.
Oriol

19

Eu segundo solução @ Ventero. Se desejar, você pode entrar em mais detalhes sobre como +converte seus operandos.

Primeiro passo (§9.1): converter os dois operandos de primitivas (valores primitivos são undefined, null, booleanos, números, cordas; todos os outros valores são objectos, incluindo matrizes e funções). Se um operando já é primitivo, você está pronto. Caso contrário, é um objeto obje as seguintes etapas são executadas:

  1. Ligue obj.valueOf(). Se retornar um primitivo, você está pronto. Instâncias diretas Objecte matrizes retornam a si mesmas, portanto você ainda não terminou.
  2. Ligue obj.toString(). Se retornar um primitivo, você está pronto. {}e []ambos retornam uma string, e pronto.
  3. Caso contrário, jogue a TypeError.

Para datas, as etapas 1 e 2 são trocadas. Você pode observar o comportamento da conversão da seguinte maneira:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Interação ( Number()primeiro converte em primitivo e depois em número):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

Segunda etapa (§11.6.1): Se um dos operandos for uma string, o outro operando também será convertido em string e o resultado será produzido pela concatenação de duas strings. Caso contrário, ambos os operandos são convertidos em números e o resultado é produzido adicionando-os.

Explicação mais detalhada do processo de conversão: “ O que é {} + {} em JavaScript? "


13

Podemos nos referir à especificação e isso é ótimo e mais preciso, mas a maioria dos casos também pode ser explicada de uma maneira mais compreensível com as seguintes declarações:

  • +e -operadores trabalham apenas com valores primitivos. Mais especificamente +(adição) funciona com cadeias ou números e +(unário) e -(subtração e unário) funciona apenas com números.
  • Todas as funções ou operadores nativos que esperam valor primitivo como argumento primeiro converterão esse argumento no tipo primitivo desejado. É feito com valueOfou toString, que estão disponíveis em qualquer objeto. Essa é a razão pela qual essas funções ou operadores não lançam erros quando invocados em objetos.

Então, podemos dizer que:

  • [] + []é o mesmo String([]) + String([])que '' + ''. Eu mencionei acima que +(adição) também é válido para números, mas não há representação numérica válida de uma matriz em JavaScript, portanto, a adição de cadeias é usada.
  • [] + {}é o mesmo String([]) + String({})que é o mesmo que'' + '[object Object]'
  • {} + []. Este merece mais explicações (veja a resposta Ventero). Nesse caso, os chavetas não são tratadas como um objeto, mas como um bloco vazio; portanto, é o mesmo que +[]. O Unary +funciona apenas com números, portanto, a implementação tenta obter um número []. Primeiro, tenta valueOfque, no caso de matrizes, retorne o mesmo objeto; depois, tenta o último recurso: a conversão de um toStringresultado em um número. Podemos escrever como +Number(String([]))qual é o mesmo +Number('')que é o mesmo que +0.
  • Array(16).join("wat" - 1)subtração -funciona apenas com números; portanto, é o mesmo que:, Array(16).join(Number("wat") - 1)pois "wat"não pode ser convertido em um número válido. Nós recebemos NaN, e qualquer operação aritmética em NaNresultados com NaN, por isso temos: Array(16).join(NaN).

0

Para reforçar o que foi compartilhado anteriormente.

A causa subjacente desse comportamento é parcialmente devida à natureza do JavaScript de tipo fraco. Por exemplo, a expressão 1 + "2" é ambígua, pois há duas interpretações possíveis com base nos tipos de operando (int, string) e (int int):

  • O usuário pretende concatenar duas strings, resultado: "12"
  • O usuário pretende adicionar dois números, resultado: 3

Assim, com diferentes tipos de entrada, as possibilidades de saída aumentam.

O algoritmo de adição

  1. Operandos de coerência para valores primitivos

As primitivas do JavaScript são string, número, nulo, indefinido e booleano (o símbolo estará disponível em breve no ES6). Qualquer outro valor é um objeto (por exemplo, matrizes, funções e objetos). O processo de coerção para converter objetos em valores primitivos é descrito assim:

  • Se um valor primitivo for retornado quando object.valueOf () for chamado, retorne esse valor, caso contrário, continue

  • Se um valor primitivo for retornado quando object.toString () for chamado, retorne esse valor, caso contrário, continue

  • Lançar um TypeError

Nota: Para valores de data, o pedido é chamar toString antes de valueOf.

  1. Se qualquer valor de operando for uma sequência, faça uma concatenação de sequência

  2. Caso contrário, converta os dois operandos em seu valor numérico e adicione esses valores

Conhecer os vários valores de coerção dos tipos no JavaScript ajuda a tornar as saídas confusas mais claras. Veja a tabela de coerção abaixo

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

Também é bom saber que o operador + do JavaScript é associativo à esquerda, pois isso determina quais serão os resultados que envolverão mais de uma operação.

Alavancar o Thus 1 + "2" fornecerá "12" porque qualquer adição que envolva uma string sempre será padronizada para a concatenação da string.

Você pode ler mais exemplos nesta postagem do blog (isenção de responsabilidade que eu escrevi).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.