Qual è la spiegazione di questi bizzarri comportamenti JavaScript menzionati nel discorso 'Wat' per CodeMash 2012?


753

Il discorso 'Wat' per CodeMash 2012 evidenzia sostanzialmente alcune strane stranezze con Ruby e JavaScript.

Ho realizzato una JSFiddle dei risultati su http://jsfiddle.net/fe479/9/ .

I comportamenti specifici di JavaScript (come non conosco Ruby) sono elencati di seguito.

Ho trovato nella JSFiddle che alcuni dei miei risultati non corrispondevano a quelli nel video, e non sono sicuro del perché. Sono, tuttavia, curioso di sapere come JavaScript sta gestendo lavorando dietro le quinte in ogni caso.

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

Sono abbastanza curioso +dell'operatore se utilizzato con array in JavaScript. Questo corrisponde al risultato del video.

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

Questo corrisponde al risultato del video. Cosa sta succedendo qui? Perché questo è un oggetto. Cosa fa l' +operatore?

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

Questo non corrisponde al video. Il video suggerisce che il risultato è 0, mentre ottengo [Oggetto].

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

Neanche questo corrisponde al video, e in che modo l'output di una variabile risulta in due oggetti? Forse il mio JSFiddle è sbagliato.

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

Facendo wat + 1 risulta in wat1wat1wat1wat1...

Ho il sospetto che questo sia solo un comportamento semplice che il tentativo di sottrarre un numero da una stringa risulta in NaN.


4
{} + [] È praticamente l'unico difficile e dipende dall'implementazione, come spiego qui , perché dipende dall'essere analizzato come una dichiarazione o come un'espressione. In quale ambiente stai testando (ho ottenuto lo 0 previsto in Firefow e Chrome ma ho ottenuto il "[oggetto oggetto]" in NodeJs)?
hugomg,

1
Sto eseguendo Firefox 9.0.1 su Windows 7 e JSFiddle lo valuta in [Oggetto]
NibblyPig

@missingno Ottengo 0 nel REPL di
NodeJS

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

1
@missingno Hai inserito la domanda qui , ma per {} + {}.
Ionică Bizău,

Risposte:


1480

Ecco un elenco di spiegazioni per i risultati che stai vedendo (e che dovresti vedere). I riferimenti che sto usando provengono dallo standard ECMA-262 .

  1. [] + []

    Quando si utilizza l'operatore addizione, entrambi gli operandi sinistro e destro vengono prima convertiti in primitivi ( §11.6.1 ). Come da §9.1 , la conversione di un oggetto (in questo caso un array) in una primitiva restituisce il suo valore predefinito, che per gli oggetti con un toString()metodo valido è il risultato della chiamata object.toString()( §8.12.8 ). Per gli array è lo stesso della chiamata array.join()( §15.4.4.2 ). Unendo un array vuoto si ottiene una stringa vuota, quindi il passaggio 7 dell'operatore addizione restituisce la concatenazione di due stringhe vuote, che è la stringa vuota.

  2. [] + {}

    Analogamente [] + [], entrambi gli operandi vengono convertiti per primi in primitivi. Per "Oggetti oggetto" (§15.2), questo è di nuovo il risultato della chiamata object.toString(), che per oggetti non nulli, non indefiniti è "[object Object]"( §15.2.4.2 ).

  3. {} + []

    Il {}qui non viene analizzato come un oggetto, ma come un blocco vuoto ( §12.1 , almeno fintanto che non stai forzando quell'affermazione ad essere un'espressione, ma ne parleremo più avanti). Il valore di ritorno dei blocchi vuoti è vuoto, quindi il risultato di tale istruzione è lo stesso di +[]. L' +operatore unario ( §11.4.6 ) ritorna ToNumber(ToPrimitive(operand)). Come già sappiamo, ToPrimitive([])è la stringa vuota, e secondo §9.3.1 , ToNumber("")è 0.

  4. {} + {}

    Simile al caso precedente, il primo {}viene analizzato come un blocco con valore di ritorno vuoto. Ancora una volta, +{}è uguale a ToNumber(ToPrimitive({})), ed ToPrimitive({})è "[object Object]"(vedi [] + {}). Quindi, per ottenere il risultato di +{}, dobbiamo applicare ToNumbersulla stringa "[object Object]". Seguendo i passaggi da §9.3.1 , otteniamo NaNdi conseguenza:

    Se la grammatica non è in grado di interpretare String come un'espansione di StringNumericLiteral , il risultato di ToNumber è NaN .

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

    Secondo §15.4.1.1 e §15.4.2.2 , Array(16)crea un nuovo array con lunghezza 16. Per ottenere il valore dell'argomento da unire, §11.6.2 passi # 5 e # 6 mostrano che dobbiamo convertire entrambi gli operandi in un numero usando ToNumber. ToNumber(1)è semplicemente 1 ( §9.3 ), mentre di ToNumber("wat")nuovo è NaNcome per §9.3.1 . Seguendo il passaggio 7 di §11.6.2 , §11.6.3 lo impone

    Se uno degli operandi è NaN , il risultato è NaN .

    Quindi l'argomento Array(16).joinè NaN. A seguito del § 15.4.4.5 ( Array.prototype.join), dobbiamo invocare ToStringl'argomento, che è "NaN"( §9.8.1 ):

    Se m è NaN , restituisce String "NaN".

    Seguendo il passaggio 10 del § 15.4.4.5 , otteniamo 15 ripetizioni della concatenazione "NaN"e della stringa vuota, che equivale al risultato che stai vedendo. Quando si utilizza "wat" + 1anziché "wat" - 1come argomento, l'operatore addizione converte 1in una stringa anziché convertirla "wat"in un numero, quindi chiama in modo efficace Array(16).join("wat1").

Sul motivo per cui stai vedendo risultati diversi per il {} + []caso: quando lo usi come argomento di funzione, stai forzando l'istruzione a essere un ExpressionStatement , il che rende impossibile analizzare {}come blocco vuoto, quindi viene invece analizzato come oggetto vuoto letterale.


2
Quindi perché [] +1 => "1" e [] -1 => -1?
Rob Elsner,

4
@RobElsner []+1segue praticamente la stessa logica di []+[], solo con 1.toString()l'operando rhs. Per []-1la spiegazione del "wat"-1punto 5. Ricordare che ToNumber(ToPrimitive([]))è 0 (punto 3).
Ventero,

4
Questa spiegazione manca / omette molti dettagli. Ad esempio "la conversione di un oggetto (in questo caso un array) in una primitiva restituisce il suo valore predefinito, che per gli oggetti con un metodo toString () valido è il risultato della chiamata a object.toString ()" manca completamente del valore di Of [] is viene chiamato per primo, ma poiché il valore restituito non è una primitiva (è un array), viene invece utilizzata la toString di []. Consiglierei di guardare questo invece per una vera spiegazione approfondita 2ality.com/2012/01/object-plus-object.html
jahav

30

Questo è più un commento che una risposta, ma per qualche ragione non posso commentare la tua domanda. Volevo correggere il tuo codice JSFiddle. Tuttavia, l'ho pubblicato su Hacker News e qualcuno mi ha suggerito di ripubblicarlo qui.

Il problema nel codice JSFiddle è che ({})(aprire parentesi graffe all'interno delle parentesi) non è lo stesso di {}(aprire parentesi graffe come l'inizio di una riga di codice). Quindi quando digiti out({} + [])stai forzando l' {}essere qualcosa che non è quando scrivi {} + []. Questo fa parte del carattere generale di Javascript.

L'idea di base era che JavaScript voleva consentire entrambi questi moduli:

if (u)
    v;

if (x) {
    y;
    z;
}

Per fare ciò, sono state fatte due interpretazioni della parentesi graffa di apertura: 1. non è richiesta e 2. può apparire ovunque .

Questa è stata una mossa sbagliata. Il codice reale non ha una parentesi graffa di apertura che appare nel mezzo del nulla e anche il codice reale tende a essere più fragile quando utilizza la prima forma anziché la seconda. (Circa una volta ogni due mesi nel mio ultimo lavoro, venivo chiamato alla scrivania di un collega quando le loro modifiche al mio codice non funzionavano, e il problema era che avevano aggiunto una linea al "se" senza aggiungere ricci parentesi graffe. Alla fine ho appena preso l'abitudine che le parentesi graffe sono sempre necessarie, anche quando stai scrivendo solo una riga.)

Fortunatamente in molti casi eval () replicherà la completezza di JavaScript. Il codice JSFiddle dovrebbe contenere:

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>");

[Anche questa è la prima volta che scrivo document.writeln in molti molti anni, e mi sento un po 'sporco a scrivere qualcosa che coinvolga sia document.writeln () che eval ().]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- non sono d'accordo (o quasi): ho spesso negli ultimi blocchi utilizzati come questo alle variabili di ambito in C . Questa abitudine è stata presa per un po 'di tempo quando si faceva C incorporato dove le variabili nello stack occupano spazio, quindi se non sono più necessarie, vogliamo che lo spazio venga liberato alla fine del blocco. Tuttavia, ECMAScript utilizza solo gli ambiti all'interno dei blocchi function () {}. Quindi, anche se non sono d'accordo sul fatto che il concetto sia sbagliato, concordo sul fatto che l'implementazione in JS sia ( probabilmente ) sbagliata.
Jess Telford,

4
@JessTelford In ES6, è possibile utilizzare letper dichiarare variabili con ambito di blocco.
Oriol,

19

Io secondo la soluzione di @ Ventero. Se lo desideri, puoi approfondire il modo in cui +converte i suoi operandi.

Prima fase (§9.1): convertire entrambi gli operandi di primitive (valori primitivi sono undefined, null, booleani, numeri, stringhe, tutti gli altri valori sono oggetti, compresi gli array e funzioni). Se un operando è già primitivo, il gioco è fatto. In caso contrario, si tratta di un oggetto obje vengono eseguite le seguenti operazioni:

  1. Chiama obj.valueOf(). Se restituisce una primitiva, il gioco è fatto. Le istanze dirette Objecte le matrici si restituiscono, quindi non hai ancora finito.
  2. Chiama obj.toString(). Se restituisce una primitiva, il gioco è fatto. {}ed []entrambi restituiscono una stringa, quindi il gioco è fatto.
  3. Altrimenti, lancia a TypeError.

Per le date, i passaggi 1 e 2 vengono scambiati. È possibile osservare il comportamento di conversione come segue:

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

Interazione ( Number()prima converte in primitivo quindi in numero):

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

Secondo passaggio (§11.6.1): Se uno degli operandi è una stringa, anche l'altro operando viene convertito in stringa e il risultato viene prodotto concatenando due stringhe. Altrimenti, entrambi gli operandi vengono convertiti in numeri e il risultato viene prodotto aggiungendoli.

Spiegazione più dettagliata del processo di conversione: " Che cos'è {} + {} in JavaScript?


13

Possiamo fare riferimento alle specifiche ed è ottimo e più accurato, ma la maggior parte dei casi può anche essere spiegata in modo più comprensibile con le seguenti affermazioni:

  • +e gli -operatori lavorano solo con valori primitivi. Più specificamente +(addizione) funziona con stringhe o numeri e +(unario) e -(sottrazione e unario) funzionano solo con i numeri.
  • Tutte le funzioni native o gli operatori che si aspettano un valore primitivo come argomento, convertiranno innanzitutto tale argomento nel tipo primitivo desiderato. Viene eseguito con valueOfo toString, che sono disponibili su qualsiasi oggetto. Questo è il motivo per cui tali funzioni o operatori non generano errori quando vengono invocati su oggetti.

Quindi possiamo dire che:

  • [] + []è lo String([]) + String([])stesso di quello '' + ''. Ho accennato in precedenza che +(aggiunta) è valida anche per i numeri, ma non esiste una rappresentazione numerica valida di un array in JavaScript, quindi viene invece utilizzata l'aggiunta di stringhe.
  • [] + {}è lo String([]) + String({})stesso di quello'' + '[object Object]'
  • {} + []. Questo merita maggiori spiegazioni (vedi la risposta di Ventero). In tal caso, le parentesi graffe non vengono trattate come un oggetto ma come un blocco vuoto, quindi risulta essere uguale a +[]. Unary +funziona solo con i numeri, quindi l'implementazione cerca di ottenere un numero da []. Prima prova valueOfche nel caso di array restituisce lo stesso oggetto, quindi prova l'ultima risorsa: la conversione di un toStringrisultato in un numero. Possiamo scrivere come +Number(String([]))che è lo stesso di quello +Number('')che è lo stesso +0.
  • Array(16).join("wat" - 1)la sottrazione -funziona solo con i numeri, quindi è la stessa di:, Array(16).join(Number("wat") - 1)poiché "wat"non può essere convertita in un numero valido. Riceviamo NaN, e qualsiasi operazione aritmetica su NaNrisultati con NaN, quindi abbiamo: Array(16).join(NaN).

0

Sostenere ciò che è stato condiviso in precedenza.

La causa di questo comportamento è in parte dovuta alla natura debolmente tipizzata di JavaScript. Ad esempio, l'espressione 1 + "2" è ambigua poiché esistono due possibili interpretazioni basate sui tipi di operando (int, string) e (int int):

  • L'utente intende concatenare due stringhe, risultato: "12"
  • L'utente intende aggiungere due numeri, risultato: 3

Pertanto, con vari tipi di input, aumentano le possibilità di output.

L'algoritmo di aggiunta

  1. Obbliga gli operandi a valori primitivi

Le primitive JavaScript sono stringa, numero, null, indefinito e booleano (Symbol arriverà presto in ES6). Qualsiasi altro valore è un oggetto (ad es. Matrici, funzioni e oggetti). Il processo di coercizione per convertire oggetti in valori primitivi è così descritto:

  • Se viene restituito un valore di base quando viene richiamato object.valueOf (), restituisce questo valore, altrimenti continua

  • Se viene restituito un valore di base quando viene richiamato object.toString (), restituisce questo valore, altrimenti continua

  • Lancia un TypeError

Nota: per i valori di data, l'ordine deve invocare a String prima di valueOf.

  1. Se qualsiasi valore di operando è una stringa, eseguire una concatenazione di stringhe

  2. Altrimenti, converti entrambi gli operandi nel loro valore numerico e quindi aggiungi questi valori

Conoscere i vari valori di coercizione dei tipi in JavaScript aiuta a rendere più chiari gli output confusi. Vedi la tabella di coercizione qui sotto

+-----------------+-------------------+---------------+
| 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           |
+-----------------+-------------------+---------------+

È anche bene sapere che l'operatore + di JavaScript è associativo di sinistra in quanto ciò determina quali saranno i risultati in casi che coinvolgono più di un'operazione +.

Sfruttando quindi 1 + "2" si otterrà "12" perché qualsiasi aggiunta che coinvolge una stringa sarà sempre predefinita alla concatenazione di stringhe.

Puoi leggere altri esempi in questo post del blog (dichiarazione di non responsabilità l'ho scritto).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.