Analyzing the JavaScript Examples in Gary Bernhardt's "Wat" Talk
This post is an homage to Gary Bernhardt's fantastic "Wat" talk in which he points out the peculiarities of some language constructs in Ruby and JavaScript. If you haven't watched the talk yet, I strongly recommend you take the time and do precisely that! It's only about 4 minutes long and highly entertaining, I promise.
In his talk, Gary shows off these four fragments of JavaScript code:
We see lots of brackets, braces, and plus signs. Here's what these fragments evaluate to:
[] + [] == ""
[] + {} == "[object Object]"
{} + [] == 0
{} + {} == NaN
When I saw these examples for the first time, I thought: "Wow, that looks messy!" The results may seem inconsistent or even arbitrary, but bear with me here. All of these examples are actually very consistent and not as bad as they look like!
#Fragment #1: [] + []
Let's start with the first fragment:
[] + [];
// ""
As we can see, applying the +
operator to two empty arrays results in an empty string. This is because the string representation of an array is the string representation of all its elements, concatenated together with commas:
[1, 2, 3].toString()
// "1,2,3"
[1, 2].toString()
// "1,2"
[1].toString()
// "1"
[].toString()
// ""
An empty array doesn't contain any elements, so its string representation is an empty string. Therefore, the concatenation of two empty strings is just another empty string.
#Fragment #2: [] + {}
So far, so good. Let's now examine the second fragment:
[] + {};
// "[object Object]"
Note that because we're not dealing with two numbers, the +
operator once again performs string concatenation rather than addition of two numeric values.
In the previous section, we've already seen that the string representation of an empty array is an empty string. The string representation of the empty object literal here is the default "[object Object]"
value. Prepending an empty string doesn't change the value, so "[object Object]"
is the final result.
In JavaScript, objects can implement a special method called toString()
which returns a custom string representation of the object the method is called on. Our empty object literal doesn't implement such a method, so we're falling back to the default implementation of the Object
prototype.
#Fragment #3: {} + []
I'd argue that so far, the results haven't been too unexpected. They've simply been following the rules of type coercion and default string representations in JavaScript.
However, {} + []
is where developers start to get confused:
{
}
+[];
// 0
Why do we see 0
(the number zero) if we type the above line into a JavaScript REPL like the browser console? Shouldn't the result be a string, just like [] + {}
was?
Before we solve the riddle, consider the three different ways the +
operator can be used:
// 1) Addition of two numeric values
2 + 2 == 4;
// 2) String concatenation of two values
(("2" + "2" ==
"22" +
// 3) Conversion of a value to a number
2) ==
2 + "2") ==
2;
In the first two cases, the +
operator is a binary operator because it has two operands (on the left and on the right). In the third case, the +
operator is a unary operator because it only has a single operand (on the right).
Also consider the two possible meanings of {}
in JavaScript. Usually, we write {}
to mean an empty object literal, but if we're in statement position, the JavaScript grammar specifies {}
to mean an empty block. The following piece of code defines two empty blocks, none of which is an object literal:
{
}
// Empty block
{
// Empty block
}
Let's take a look at our fragment again:
{
}
+[];
Let me change the whitespace a little bit to make it clearer how the JavaScript engine sees the code:
{
// Empty block
}
+[];
Now we can clearly see what's happening here. We have a block statement followed by another statement that contains a unary +
expression operating on an empty array. The trailing semicolon is inserted automatically according to the rules of ASI (automatic semicolon insertion).
You can easily verify in your browser console that +[]
evaluates to 0
. The empty array has an empty string as its string representation, which in turn is converted to the number zero by the +
operator. Finally, the value of the last statement (+[]
, in this case) is reported by the browser console.
Alternatively, you could feed both code snippets to a JavaScript parser such as Esprima and compare the resulting abstract syntax trees. Here's the AST for [] + {}
:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "ArrayExpression",
"elements": []
},
"right": {
"type": "ObjectExpression",
"properties": []
}
}
}
],
"sourceType": "script"
}
And here's the AST for {} + []
:
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"body": []
},
{
"type": "ExpressionStatement",
"expression": {
"type": "UnaryExpression",
"operator": "+",
"argument": {
"type": "ArrayExpression",
"elements": []
},
"prefix": true
}
}
],
"sourceType": "script"
}
The confusion stems from a nuance of the JavaScript grammar that uses braces both for object literals and blocks. In statement position, an opening brace starts a block, while in expression position an opening brace starts an object literal.
#Fragment #4: {} + {}
Finally, let's quickly take a look at our last fragment {} + {}
:
{
}
+{};
// NaN
Well, adding two object literals is literally "not a number" — but are we adding two object literals here? Don't let the braces fool you again! This is what's happening:
{
// Empty block
}
+{};
It's pretty much the same deal as in the previous example. However, we're now applying the unary plus operator to an empty object literal. That's basically the same as doing Number({})
, which results in NaN
because our object literal cannot be converted to a number.
If you want the JavaScript engine to parse the code as two empty object literals, wrap the first one (or the entire piece of code) within parentheses. You should now see the expected result:
({} +
{}(
// "[object Object][object Object]"
{} + {},
));
// "[object Object][object Object]"
The opening parenthesis causes the parser to attempt to recognize an expression, which is why it doesn't treat the {}
as a block (which would be a statement).
#Summary
You should now see why the four code fragments evaluate the way they do. It's not arbitrary or random at all; the rules of type coercion are applied exactly as laid out in the specification and the language grammar.
Just keep in mind that if an opening brace is the first character to appear in a statement, it'll be interpreted as the start of a block rather than an object literal.