Was ist die Erklärung für diese bizarren JavaScript-Verhaltensweisen, die im 'Wat'-Vortrag für CodeMash 2012 erwähnt wurden?


753

Der 'Wat'-Vortrag für CodeMash 2012 weist im Grunde genommen auf einige bizarre Macken mit Ruby und JavaScript hin.

Ich habe eine JSFiddle der Ergebnisse unter http://jsfiddle.net/fe479/9/ erstellt .

Die für JavaScript spezifischen Verhaltensweisen (da ich Ruby nicht kenne) sind unten aufgeführt.

Ich habe in der JSFiddle festgestellt, dass einige meiner Ergebnisse nicht mit denen im Video übereinstimmen, und ich bin mir nicht sicher, warum. Ich bin jedoch gespannt, wie JavaScript jeweils hinter den Kulissen arbeitet.

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

Ich bin ziemlich neugierig auf den +Operator, wenn er mit Arrays in JavaScript verwendet wird. Dies entspricht dem Ergebnis des Videos.

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

Dies entspricht dem Ergebnis des Videos. Was ist denn hier los? Warum ist das ein Objekt? Was macht der +Bediener?

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

Dies stimmt nicht mit dem Video überein. Das Video legt nahe, dass das Ergebnis 0 ist, während ich [Objekt] erhalte.

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

Dies stimmt auch nicht mit dem Video überein. Wie führt die Ausgabe einer Variablen zu zwei Objekten? Vielleicht ist meine JSFiddle falsch.

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

Wat + 1 führt zu wat1wat1wat1wat1...

Ich vermute, dass dies nur ein einfaches Verhalten ist, bei dem der Versuch, eine Zahl von einer Zeichenfolge zu subtrahieren, zu NaN führt.


4
Das {} + [] ist im Grunde das einzige knifflige und implementierungsabhängige, wie ich hier erkläre , da es davon abhängt, als Anweisung oder als Ausdruck analysiert zu werden. In welcher Umgebung testen Sie (ich habe die erwartete 0 in Firefow und Chrome erhalten, aber das "[Objekt Objekt]" in NodeJs)?
Hugomg

1
Ich führe Firefox 9.0.1 unter Windows 7 aus und JSFiddle wertet es als [Objekt] aus
NibblyPig

@missingno Ich bekomme 0 in der NodeJS REPL
OrangeDog

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

1
@missingno Habe die Frage hier gepostet , aber für {} + {}.
Ionică Bizău

Antworten:


1479

Hier ist eine Liste mit Erklärungen für die Ergebnisse, die Sie sehen (und sehen sollen). Die Referenzen, die ich verwende, stammen aus dem ECMA-262-Standard .

  1. [] + []

    Bei Verwendung des Additionsoperators werden sowohl der linke als auch der rechte Operand zuerst in Grundelemente konvertiert ( §11.6.1 ). Wie pro §9.1 , ein Objekt , Umwandeln (in diesem Fall ein Array) auf einen primitiven kehrt seinen Standardwert, der für Objekte mit einer gültigen toString()Methode das Ergebnis des Aufrufs ist object.toString()( §8.12.8 ). Für Arrays entspricht dies dem Aufruf array.join()( §15.4.4.2 ). Das Verbinden eines leeren Arrays führt zu einer leeren Zeichenfolge. Schritt 7 des Additionsoperators gibt also die Verkettung von zwei leeren Zeichenfolgen zurück, bei denen es sich um die leere Zeichenfolge handelt.

  2. [] + {}

    Ähnlich wie bei [] + []werden beide Operanden zuerst in Grundelemente konvertiert. Für "Objektobjekte" (§15.2) ist dies wiederum das Ergebnis des Aufrufs object.toString(), was für nicht null, nicht undefinierte Objekte "[object Object]"( §15.2.4.2 ) ist.

  3. {} + []

    Das {}hier wird nicht als Objekt analysiert, sondern als leerer Block ( §12.1 , zumindest solange Sie diese Anweisung nicht als Ausdruck erzwingen, sondern dazu später mehr). Der Rückgabewert von leeren Blöcken ist leer, daher ist das Ergebnis dieser Anweisung dasselbe wie +[]. Der unäre +Operator ( §11.4.6 ) kehrt zurück ToNumber(ToPrimitive(operand)). Wie wir bereits wissen, ToPrimitive([])ist die leere Zeichenkette, und nach §9.3.1 , ToNumber("")ist 0.

  4. {} + {}

    Ähnlich wie im vorherigen Fall wird der erste {}als Block mit leerem Rückgabewert analysiert. Wieder +{}ist das gleiche wie ToNumber(ToPrimitive({}))und ToPrimitive({})ist "[object Object]"(siehe [] + {}). Um das Ergebnis von zu erhalten +{}, müssen wir ToNumberauf die Zeichenfolge anwenden "[object Object]". Wenn wir die Schritte aus §9.3.1 befolgen , erhalten wir NaNals Ergebnis:

    Wenn die Grammatik des String als Erweiterung nicht interpretieren kann StringNumericLiteral , dann ist das Ergebnis von ToNumber ist NaN .

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

    Wie pro §15.4.1.1 und §15.4.2.2 , Array(16)ein neues Array mit einer Länge erzeugt 16. Um den Wert des Arguments zu erhalten zu verbinden, §11.6.2 Schritte # 5 und 6 zeigt # , dass wir beiden Operanden in einem konvertieren Nummer mit ToNumber. ToNumber(1)ist einfach 1 ( §9.3 ), während ToNumber("wat")wiederum NaNgemäß §9.3.1 . Nach dem Schritt 7 von §11.6.2 , §11.6.3 diktiert , dass

    Wenn einer der Operanden NaN ist , ist das Ergebnis NaN .

    Das Argument dazu Array(16).joinist also NaN. Nach §15.4.4.5 ( Array.prototype.join) müssen wir ToStringauf das Argument "NaN"( §9.8.1 ) zurückgreifen :

    Wenn m ist NaN , geben Sie den String "NaN".

    Nach Schritt 10 von §15.4.4.5 erhalten wir 15 Wiederholungen der Verkettung von "NaN"und der leeren Zeichenfolge, was dem angezeigten Ergebnis entspricht. Bei Verwendung von "wat" + 1anstelle von "wat" - 1als Argument wird der Additionsoperator 1in eine Zeichenfolge konvertiert , anstatt "wat"in eine Zahl zu konvertieren , sodass er effektiv aufruft Array(16).join("wat1").

Warum Sie für den {} + []Fall unterschiedliche Ergebnisse sehen : Wenn Sie es als Funktionsargument verwenden, erzwingen Sie, dass die Anweisung ein ExpressionStatement ist , wodurch es unmöglich wird, sie {}als leeren Block zu analysieren , sodass sie stattdessen als leeres Objekt analysiert wird wörtlich.


2
Warum ist [] +1 => "1" und [] -1 => -1?
Rob Elsner

4
@RobElsner []+1folgt so ziemlich der gleichen Logik wie []+[], nur mit 1.toString()als rhs-Operand. Für []-1sieht die Erklärung "wat"-1in Punkt 5 , dass Denken Sie daran , ToNumber(ToPrimitive([]))ist 0 (Punkt 3).
Ventero

4
Diese Erklärung fehlt / lässt viele Details aus. Beispiel: "Das Konvertieren eines Objekts (in diesem Fall eines Arrays) in ein Grundelement gibt seinen Standardwert zurück, der für Objekte mit einer gültigen toString () -Methode das Ergebnis des Aufrufs von object.toString () ist." Dieser Wert von [] fehlt vollständig wird zuerst aufgerufen, aber da der Rückgabewert kein Grundelement ist (es ist ein Array), wird stattdessen der toString von [] verwendet. Ich würde empfehlen, dies stattdessen für eine wirklich ausführliche Erklärung zu suchen. 2ality.com/2012/01/object-plus-object.html
jahav

30

Dies ist eher ein Kommentar als eine Antwort, aber aus irgendeinem Grund kann ich Ihre Frage nicht kommentieren. Ich wollte Ihren JSFiddle-Code korrigieren. Ich habe dies jedoch in den Hacker News gepostet und jemand hat vorgeschlagen, es hier erneut zu veröffentlichen.

Das Problem im JSFiddle-Code besteht darin, dass ({})(Öffnen von Klammern in Klammern) nicht dasselbe ist wie {}(Öffnen von Klammern als Anfang einer Codezeile). Wenn Sie also tippen out({} + []), zwingen Sie das {}, etwas zu sein, was es nicht ist, wenn Sie tippen {} + []. Dies ist Teil der allgemeinen Wasserqualität von Javascript.

Die Grundidee war einfaches JavaScript, das beide Formen zulassen wollte:

if (u)
    v;

if (x) {
    y;
    z;
}

Zu diesem Zweck wurden zwei Interpretationen der Öffnungsklammer vorgenommen: 1. Sie ist nicht erforderlich und 2. Sie kann überall auftreten .

Dies war ein falscher Schritt. Echter Code hat keine öffnende Klammer mitten im Nirgendwo, und realer Code ist auch fragiler, wenn er die erste Form anstelle der zweiten verwendet. (Ungefähr alle zwei Monate bei meinem letzten Job wurde ich an den Schreibtisch eines Kollegen gerufen, wenn die Änderungen an meinem Code nicht funktionierten, und das Problem war, dass sie dem "Wenn" eine Zeile hinzugefügt hatten, ohne Curly hinzuzufügen Ich habe mir schließlich angewöhnt, dass die geschweiften Klammern immer erforderlich sind, auch wenn Sie nur eine Zeile schreiben.)

Glücklicherweise repliziert eval () in vielen Fällen die volle Leistung von JavaScript. Der JSFiddle-Code sollte lauten:

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

[Außerdem ist es das erste Mal seit vielen, vielen Jahren, dass ich document.writeln geschrieben habe, und ich fühle mich ein wenig schmutzig, wenn ich etwas schreibe, das sowohl document.writeln () als auch eval () betrifft.]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Ich bin anderer Meinung (irgendwie): Ich habe in der Vergangenheit oft solche Blöcke verwendet, um Variablen in C zu erfassen . Diese Angewohnheit wurde vor einiger Zeit beim Einbetten von C aufgegriffen, bei dem Variablen auf dem Stapel Speicherplatz beanspruchen. Wenn sie also nicht mehr benötigt werden, möchten wir, dass der Speicherplatz am Ende des Blocks freigegeben wird. ECMAScript umfasst jedoch nur Bereiche innerhalb von function () {} -Blöcken. Obwohl ich nicht der Meinung bin, dass das Konzept falsch ist, stimme ich zu, dass die Implementierung in JS ( möglicherweise ) falsch ist.
Jess Telford

4
@JessTelford In ES6 können Sie letVariablen mit Blockbereich deklarieren.
Oriol

19

Ich stimme der Lösung von @ Ventero zu. Wenn Sie möchten, können Sie detaillierter darauf eingehen, wie +die Operanden konvertiert werden.

Erster Schritt (§9.1): konvertiert beide Operanden in Primitiven (primitive Werte sind undefined, null, booleans, Zahlen, Strings, alle anderen Werte sind Objekte, einschließlich Arrays und Funktionen). Wenn ein Operand bereits primitiv ist, sind Sie fertig. Wenn nicht, handelt es sich um ein Objekt, objund die folgenden Schritte werden ausgeführt:

  1. Rufen Sie an obj.valueOf(). Wenn es ein Grundelement zurückgibt, sind Sie fertig. Direkte Instanzen von Objectund Arrays geben sich selbst zurück, sodass Sie noch nicht fertig sind.
  2. Rufen Sie an obj.toString(). Wenn es ein Grundelement zurückgibt, sind Sie fertig. {}und []beide geben einen String zurück, damit Sie fertig sind.
  3. Andernfalls werfen Sie eine TypeError.

Für Daten werden Schritt 1 und 2 vertauscht. Sie können das Konvertierungsverhalten wie folgt beobachten:

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

Interaktion ( Number()zuerst in primitiv, dann in Zahl konvertiert):

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

Zweiter Schritt (§11.6.1): Wenn einer der Operanden eine Zeichenfolge ist, wird der andere Operand ebenfalls in eine Zeichenfolge konvertiert und das Ergebnis durch Verketten von zwei Zeichenfolgen erzeugt. Andernfalls werden beide Operanden in Zahlen konvertiert und das Ergebnis durch Hinzufügen erzeugt.

Detailliertere Erläuterung des Konvertierungsprozesses: „ Was ist {} + {} in JavaScript?


13

Wir können uns auf die Spezifikation beziehen und das ist großartig und am genauesten, aber die meisten Fälle können auch mit den folgenden Aussagen verständlicher erklärt werden:

  • +und -Operatoren arbeiten nur mit primitiven Werten. Insbesondere +funktioniert (Addition) entweder mit Zeichenfolgen oder Zahlen, und +(unär) und -(Subtraktion und unär) funktionieren nur mit Zahlen.
  • Alle nativen Funktionen oder Operatoren, die einen primitiven Wert als Argument erwarten, konvertieren dieses Argument zuerst in den gewünschten primitiven Typ. Dies geschieht mit valueOfoder toString, die für jedes Objekt verfügbar sind. Dies ist der Grund, warum solche Funktionen oder Operatoren beim Aufrufen von Objekten keine Fehler auslösen.

Also können wir das sagen:

  • [] + []ist das gleiche wie String([]) + String([])das gleiche wie '' + ''. Ich habe oben erwähnt, dass +(Addition) auch für Zahlen gültig ist, aber es gibt keine gültige Zahlendarstellung eines Arrays in JavaScript, daher wird stattdessen das Hinzufügen von Zeichenfolgen verwendet.
  • [] + {}ist das gleiche wie String([]) + String({})das gleiche wie'' + '[object Object]'
  • {} + []. Dieser verdient mehr Erklärung (siehe Antwort von Ventero). In diesem Fall werden geschweifte Klammern nicht als Objekt, sondern als leerer Block behandelt, sodass sich herausstellt, dass es dasselbe ist wie +[]. Unary +funktioniert nur mit Zahlen, daher versucht die Implementierung, eine Zahl herauszuholen []. Zuerst wird versucht, valueOfwelches bei Arrays dasselbe Objekt zurückgibt, und dann wird der letzte Ausweg versucht: die Umwandlung eines toStringErgebnisses in eine Zahl. Wir können es so schreiben, wie +Number(String([]))es dasselbe ist wie +Number('')was dasselbe ist wie +0.
  • Array(16).join("wat" - 1)Die Subtraktion -funktioniert nur mit Zahlen, ist also dasselbe wie : Array(16).join(Number("wat") - 1), da "wat"sie nicht in eine gültige Zahl umgewandelt werden kann. Wir erhalten NaNund jede arithmetische Operation auf NaNErgebnisse mit NaN, also haben wir : Array(16).join(NaN).

0

Um zu stützen, was früher geteilt wurde.

Die zugrunde liegende Ursache für dieses Verhalten ist teilweise auf die schwach typisierte Natur von JavaScript zurückzuführen. Beispielsweise ist der Ausdruck 1 + "2" nicht eindeutig, da es zwei mögliche Interpretationen gibt, die auf den Operandentypen (int, string) und (int int) basieren:

  • Der Benutzer beabsichtigt, zwei Zeichenfolgen zu verketten, Ergebnis: "12"
  • Der Benutzer beabsichtigt, zwei Zahlen hinzuzufügen, Ergebnis: 3

Somit erhöhen sich mit unterschiedlichen Eingabetypen die Ausgabemöglichkeiten.

Der Additionsalgorithmus

  1. Operanden zu primitiven Werten zwingen

Die JavaScript-Grundelemente sind string, number, null, undefined und boolean (Symbol wird in Kürze in ES6 verfügbar sein). Jeder andere Wert ist ein Objekt (z. B. Arrays, Funktionen und Objekte). Der Zwangsprozess zum Umwandeln von Objekten in primitive Werte wird folgendermaßen beschrieben:

  • Wenn beim Aufrufen von object.valueOf () ein primitiver Wert zurückgegeben wird, geben Sie diesen Wert zurück, andernfalls fahren Sie fort

  • Wenn beim Aufrufen von object.toString () ein primitiver Wert zurückgegeben wird, geben Sie diesen Wert zurück, andernfalls fahren Sie fort

  • Wirf einen TypeError

Hinweis: Bei Datumswerten muss die Reihenfolge toString vor valueOf aufgerufen werden.

  1. Wenn ein Operandenwert eine Zeichenfolge ist, führen Sie eine Zeichenfolgenverkettung durch

  2. Andernfalls konvertieren Sie beide Operanden in ihren numerischen Wert und fügen Sie diese Werte hinzu

Wenn Sie die verschiedenen Zwangswerte von Typen in JavaScript kennen, werden die verwirrenden Ausgaben klarer. Siehe die Zwangstabelle unten

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

Es ist auch gut zu wissen, dass der JavaScript-Operator + linksassoziativ ist, da dies bestimmt, welche Ausgabe Fälle mit mehr als einer + Operation sein werden.

Wenn Sie also 1 + "2" verwenden, erhalten Sie "12", da bei jeder Addition mit einer Zeichenfolge standardmäßig die Zeichenfolgenverkettung verwendet wird.

Weitere Beispiele finden Sie in diesem Blog-Beitrag (Haftungsausschluss, den ich geschrieben habe).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.