Dieser Beitrag ist eine Hommage an Gary Bernhardts fantastischen „Wat“ -Vortrag, in dem er auf die Besonderheiten einiger Sprachkonstrukte in Ruby und JavaScript hinweist. Wenn Sie den Vortrag noch nicht gesehen haben, empfehle ich Ihnen dringend, sich die Zeit zu nehmen und genau das zu tun! Es ist nur etwa 4 Minuten lang und sehr unterhaltsam, das verspreche ich.
In seinem Vortrag zeigt Gary diese vier Fragmente von JavaScript-Code:
sehen wir viele Klammern, geschweifte Klammern und Pluszeichen. Hier ist, was diese Fragmente bedeuten:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
Als ich diese Beispiele zum ersten Mal sah, dachte ich: „Wow, das sieht chaotisch aus!“ Die Ergebnisse mögen widersprüchlich oder sogar willkürlich erscheinen, aber tragen Sie mit mir hier. Alle diese Beispiele sind tatsächlich sehr konsistent und nicht so schlecht, wie sie aussehen!
#Fragment #1: +
Beginnen wir mit dem ersten Fragment:
+ // ""
Wie wir sehen können, führt das Anwenden des Operators +
auf zwei leere Arrays zu einer leeren Zeichenfolge. Dies liegt daran, dass die Zeichenfolgendarstellung eines Arrays die Zeichenfolgendarstellung aller seiner Elemente ist, die mit Kommas verkettet sind:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
Ein leeres Array enthält keine Elemente, daher ist seine Zeichenfolgendarstellung eine leere Zeichenfolge. Daher ist die Verkettung zweier leerer Zeichenfolgen nur eine weitere leere Zeichenfolge.
#Fragment #2: + {}
So weit, so gut. Betrachten wir nun das zweite Fragment:
+ {}// ""
Da es sich nicht um zwei Zahlen handelt, führt der Operator +
erneut eine Zeichenfolgenverkettung durch, anstatt zwei numerische Werte hinzuzufügen.
Im vorherigen Abschnitt haben wir bereits gesehen, dass die Zeichenfolgendarstellung eines leeren Arrays eine leere Zeichenfolge ist. Die Zeichenfolgendarstellung des leeren Objektliterals ist hier der Standardwert ""
. Das Voranstellen einer leeren Zeichenfolge ändert den Wert nicht, daher ist ""
das Endergebnis.
In JavaScript können Objekte eine spezielle Methode namens toString()
implementieren, die eine benutzerdefinierte Zeichenfolgendarstellung des Objekts zurückgibt, für das die Methode aufgerufen wird. Unser leeres Objektliteral implementiert eine solche Methode nicht, daher greifen wir auf die Standardimplementierung des Prototyps Object
zurück.
#Fragment #3: {} +
Ich würde argumentieren, dass die Ergebnisse bisher nicht allzu unerwartet waren. Sie haben einfach die Regeln des Typzwangs und der Standardzeichenfolgendarstellungen in JavaScript befolgt.
Bei {} +
werden Entwickler jedoch verwirrt:
{} + // 0
Warum sehen wir 0
(die Zahl Null), wenn wir die obige Zeile in eine JavaScript-REPL wie die Browserkonsole eingeben? Sollte das Ergebnis nicht eine Zeichenfolge sein, genau wie + {}
?
Bevor wir das Rätsel lösen, betrachten wir die drei verschiedenen Möglichkeiten, wie der Operator +
verwendet werden kann:
// 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
In den ersten beiden Fällen ist der Operator +
ein binärer Operator, da er zwei Operanden hat (links und rechts). Im dritten Fall ist der Operator +
ein unärer Operator, da er nur einen einzigen Operanden hat (rechts).
Betrachten Sie auch die beiden möglichen Bedeutungen von {}
in JavaScript. Normalerweise schreiben wir {}
, um ein leeres Objektliteral zu bedeuten, aber wenn wir uns in der Anweisungsposition befinden, gibt die JavaScript-Grammatik {}
an, um einen leeren Block zu bedeuten. Der folgende Code definiert zwei leere Blöcke, von denen keiner ein Objektliteral ist:
{}// Empty block{ // Empty block}
Schauen wir uns unser Fragment noch einmal an:
{} +
Lassen Sie mich das Leerzeichen ein wenig ändern, um klarer zu machen, wie die JavaScript-Engine den Code sieht:
{ // Empty block}+;
Jetzt können wir klar sehen, was hier passiert. Wir haben eine Block-Anweisung, gefolgt von einer anderen Anweisung, die einen unären +
-Ausdruck enthält, der auf einem leeren Array arbeitet. Das nachfolgende Semikolon wird automatisch nach den Regeln von ASI (Automatic Semicolon Insertion) eingefügt.
Sie können in Ihrer Browserkonsole leicht überprüfen, ob +
zu 0
ausgewertet wird. Das leere Array hat eine leere Zeichenfolge als Zeichenfolgendarstellung, die wiederum durch den Operator +
in die Zahl Null konvertiert wird. Schließlich wird der Wert der letzten Anweisung (in diesem Fall+
) von der Browserkonsole gemeldet.
Alternativ können Sie beide Codeausschnitte einem JavaScript-Parser wie Esprima zuführen und die resultierenden abstrakten Syntaxbäume vergleichen. Hier ist der AST für + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
Und hier ist der AST für {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
Die Verwirrung beruht auf einer Nuance der JavaScript-Grammatik, die Klammern sowohl für Objektliterale als auch für Blöcke verwendet. In der Anweisungsposition startet eine öffnende Klammer einen Block, während in der Ausdrucksposition eine öffnende Klammer ein Objektliteral startet.
#Fragment #4: {} + {}
Schauen wir uns zum Schluss schnell unser letztes Fragment an {} + {}
:
{} + {}// NaN
Nun, das Hinzufügen von zwei Objektliteralen ist buchstäblich „keine Zahl“ – aber fügen wir hier zwei Objektliterale hinzu? Lass dich nicht wieder von den Zahnspangen täuschen! Dies ist, was passiert:
{ // Empty block}+{};
Es ist so ziemlich der gleiche Deal wie im vorherigen Beispiel. Wir wenden jetzt jedoch den unären Plus-Operator auf ein leeres Objektliteral an. Das ist im Grunde dasselbe wie Number({})
, was zu NaN
da unser Objektliteral nicht in eine Zahl konvertiert werden kann.
Wenn die JavaScript-Engine den Code als zwei leere Objektliterale analysieren soll, schließen Sie den ersten (oder den gesamten Code) in Klammern ein. Sie sollten nun das erwartete Ergebnis sehen:
({}) + {}// ""({} + {})// ""
Die öffnende Klammer bewirkt, dass der Parser versucht, einen Ausdruck zu erkennen, weshalb {}
nicht als Block behandelt wird (was eine Anweisung wäre).
#Summary
Sie sollten jetzt sehen, warum die vier Codefragmente so ausgewertet werden, wie sie es tun. Es ist überhaupt nicht willkürlich oder zufällig; Die Regeln des Typzwangs werden genau so angewendet, wie sie in der Spezifikation und der Sprachgrammatik festgelegt sind.
Wenn eine öffnende Klammer das erste Zeichen in einer Anweisung ist, wird sie als Beginn eines Blocks und nicht als Objektliteral interpretiert.