detta inlägg är en hyllning till Gary Bernhardts fantastiska” Wat ” – tal där han påpekar särdragen hos vissa språkkonstruktioner i Ruby och JavaScript. Om du inte har sett samtalet ännu rekommenderar jag starkt att du tar dig tid och gör just det! Det är bara ca 4 minuter lång och mycket underhållande, Jag lovar.
i sitt föredrag visar Gary upp dessa fyra fragment av JavaScript-kod:
vi ser massor av parenteser, hängslen och plustecken. Här är vad dessa fragment utvärderar till:
+ == ""
+ {} == ""
{} + == 0
{} + {} == NaN
när jag såg dessa exempel för första gången tänkte jag: ”Wow, det ser rörigt ut!”Resultaten kan verka inkonsekventa eller till och med godtyckliga, men bära med mig här. Alla dessa exempel är faktiskt mycket konsekventa och inte så dåliga som de ser ut!
#Fragment #1: +
låt oss börja med det första fragmentet:
+ // ""
som vi kan se resulterar det i en tom sträng att använda +
– operatören på två tomma arrayer. Detta beror på att strängrepresentationen av en array är strängrepresentationen av alla dess element, sammanfogade tillsammans med kommatecken:
.toString()// "1,2,3".toString()// "1,2".toString()// "1".toString()// ""
en tom array innehåller inga element, så dess strängrepresentation är en tom sträng. Därför är sammanfogningen av två tomma strängar bara en annan tom sträng.
#Fragment #2: + {}
hittills, så bra. Låt oss nu undersöka det andra fragmentet:
+ {}// ""
Observera att eftersom vi inte har att göra med två siffror, utför operatören +
återigen strängsammanfogning snarare än tillägg av två numeriska värden.
i föregående avsnitt har vi redan sett att strängrepresentationen av en tom array är en tom sträng. Strängrepresentationen av det tomma objektet bokstavligt här är standardvärdet ""
. Prepending en tom sträng ändrar inte värdet, så ""
är det slutliga resultatet.
i JavaScript kan objekt implementera en speciell metod som heter toString()
som returnerar en anpassad strängrepresentation av objektet som metoden anropas. Vårt tomma objekt literal implementerar inte en sådan metod, så vi faller tillbaka till standardimplementeringen av prototypen Object
.
#Fragment #3: {} +
jag skulle hävda att resultaten hittills inte har varit för oväntade. De har helt enkelt följt reglerna för typ tvång och standardsträngrepresentationer i JavaScript.
men {} +
är där utvecklare börjar bli förvirrade:
{} + // 0
Varför ser vi 0
(numret noll) om vi skriver ovanstående rad i en JavaScript REPL som webbläsarkonsolen? Borde inte resultatet vara en sträng, precis som + {}
var?
innan vi löser gåtan, överväga de tre olika sätten +
– operatören kan användas:
// 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
i de två första fallen är +
– operatören en binär operatör eftersom den har två operander (till vänster och till höger). I det tredje fallet är +
– operatören en unär operatör eftersom den bara har en enda operand (till höger).
tänk också på de två möjliga betydelserna av {}
i JavaScript. Vanligtvis skriver vi {}
för att betyda ett tomt objekt bokstavligt, men om vi är i uttalandeposition anger JavaScript-grammatiken {}
för att betyda ett tomt block. Följande kodstycke definierar två tomma block, varav inget är ett objekt bokstavligt:
{}// Empty block{ // Empty block}
Låt oss ta en titt på vårt fragment igen:
{} +
Låt mig ändra whitespace lite för att göra det tydligare hur JavaScript-motorn ser koden:
{ // Empty block}+;
nu kan vi tydligt se vad som händer här. Vi har ett blockuttalande följt av ett annat uttalande som innehåller ett unary +
– uttryck som fungerar på en tom array. Den efterföljande semikolonen infogas automatiskt enligt reglerna för ASI (automatisk semikoloninsättning).
du kan enkelt verifiera i din webbläsarkonsol att +
utvärderar till 0
. Den tomma matrisen har en tom sträng som sin strängrepresentation, som i sin tur konverteras till siffran noll av operatören +
. Slutligen rapporteras värdet av det sista uttalandet (+
, i detta fall) av webbläsarkonsolen.
Alternativt kan du mata båda kodavsnitten till en JavaScript-parser som Esprima och jämföra de resulterande abstrakta syntaxträden. Här är AST för + {}
:
{ "type": "Program", "body": }, "right": { "type": "ObjectExpression", "properties": } } } ], "sourceType": "script"}
och här är AST för {} +
:
{ "type": "Program", "body": }, { "type": "ExpressionStatement", "expression": { "type": "UnaryExpression", "operator": "+", "argument": { "type": "ArrayExpression", "elements": }, "prefix": true } } ], "sourceType": "script"}
förvirringen härrör från en nyans av JavaScript-grammatiken som använder hängslen både för objektbokstavar och block. I uttalande position, en öppning stag startar ett block, medan i expression position en öppning stag startar ett objekt bokstav.
#Fragment #4: {} + {}
slutligen, låt oss snabbt ta en titt på vårt sista fragment {} + {}
:
{} + {}// NaN
Tja, att lägga till två objektbokstäver är bokstavligen ”inte ett tal” – men lägger vi till två objektbokstäver här? Låt inte hängslen lura dig igen! Detta är vad som händer:
{ // Empty block}+{};
det är ungefär samma affär som i föregående exempel. Men vi tillämpar nu unary plus-operatören på ett tomt objekt bokstavligt. Det är i princip detsamma som att göra Number({})
, vilket resulterar i NaN
eftersom vårt objekt bokstavligt inte kan konverteras till ett tal.
om du vill att JavaScript-motorn ska tolka koden som två tomma objektbokstavar, linda in den första (eller hela koden) inom parentes. Du bör nu se det förväntade resultatet:
({}) + {}// ""({} + {})// ""
öppningsparentesen får parsern att försöka känna igen ett uttryck, varför det inte behandlar {}
som ett block (vilket skulle vara ett uttalande).
#sammanfattning
du bör nu se varför de fyra kodfragmenten utvärderar hur de gör. Det är inte godtyckligt eller slumpmässigt alls; reglerna för typtvång tillämpas exakt som anges i specifikationen och språkgrammatiken.
tänk bara på att om en öppningsstång är det första tecknet som visas i ett uttalande, kommer det att tolkas som början på ett block snarare än ett objekt bokstavligt.