TL;DR: In Chrome or Safari’s JavaScript console (or in jsc
, but not node
), run the following:
1 2 3 4 |
|
a) Why is the first result wrong?
b) Why does storing it to a variable (observing it) change it?
(The answers are below.)
My friend Anton pointed out to me this short ‘WAT’ talk by Gary Bernhardt from 2012. It did something that confused me somewhat. Here’s what Gary’s example was:
1 2 3 4 5 6 7 8 9 |
|
Now 1 and 2 are fine, but 3 and 4 immediately went against what I was expecting - it truly was a ‘wat’ moment. I noticed was he was running this in JavaScriptCore, let’s see what Node.js makes of it:
1 2 3 4 5 6 7 8 9 |
|
Node.js seems to be doing exactly what I’d expect - using the
+
as string concatenation (since the leading argument is not numeric).
So it’s calling .toString()
on everything to produce the results you
see above (an Array’s toString
is effectively return this.join(",")
,
and since it’s empty it just returns the empty string).
So what’s going on?! Let’s try V8 (the engine behind Node.js) in the browser (Chrome):
1 2 3 4 |
|
So… V8 in the browser agrees with JSC, not Node, and is behaving in this strange fashion. But wait!
1 2 3 4 |
|
In the words of Gary: WAT?! By storing the value to a variable I’ve changed it’s value? This immediately reminds me of the Heisenberg Uncertainty Principle - that by observing something you implicitly change what it is; only in this case the observation is effectively storing it to a variable. Quantum JavaScript?
If you want to figure this out yourself, take a while to ponder this now before reading on…
Read on for the spoiler
Digging further, I realise that I could reproduce this by calling eval
(which is basically what the E in REPL stands for). This allowed me to
reproduce the same thing in Node:
1 2 3 4 5 6 7 8 |
|
At this pointed I cheated: I hopped on over to #javascript on irc.freenode.net and asked there. After some pondering, ckknight figured it out (with a little help from DDR_ and Havvy). Here’s the highlights:
DDR_ : Benjie: I think that {} is interpreted specially somehow in the console.
Havvy : >> ({}) + ({})
ecmabot : Havvy: (string) '[object Object][object Object]'
Havvy : >> {}
ecmabot : Havvy: undefined
ckknight : I get it
DDR_ : I'm guessing it's ambiguous or something.
ckknight : {} + {} turns into {}; (+{})
ckknight : since the first is interpreted as a block statement
ckknight : and then you have a unary + and an object
You can read the full transcript at the bottom of this post.
ckknight figured out that the first {}
was being interpretted as a
code block, rather than an object literal, and JavaScript’s automatic
semicolon insertion (ASI) was taking over and changing the code to be
interpretted like so:
1 2 3 4 |
|
The {};
is ignored (empty code block), and the leading +
coerces
the values to a number, which is equivalent to:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
(Aside: to expand on the meaning of calling Number
above, read about
Number called as a function, ToNumber, ToPrimitive,
and finally DefaultValue.)
So - mystery solved: storing to a variable gives the JavaScript
interpretter enough context to interpret the first {}
as an object
literal and not a code block.
And I’m guessing that Node’s REPL, rather than calling plain eval("{} +
{}")
, does something a little more complex - perhaps like this:
1
|
|
thereby (coincidentally?) side-stepping the issue.
IRC Transcript
Benjie : When I type `{} + {}` into the javascript console (JSC or
> Chrome inspector or Safari inspector; note: not node.js REPL) I get
> `NaN` - why is this not the same as when I type `var a = {} + {}; a`
> (which gives me `[object Object][object Object]`?
Benjie : i.e. why does storing it to a variable somehow change it's
> value. (Or more specifically, at a guess, how does the javascript
> console coerce values and why isn't it the same as normal coercion?)
ckknight : Benjie: seems like an implementation flaw, like a bug seeped in
DDR_ : Benjie: I think that {} is interpreted specially somehow in the console.
Havvy : >> var a = {} + {}; a
ecmabot : Havvy: (string) '[object Object][object Object]'
Havvy : >> {} + {}
ecmabot : Havvy: (number) NaN
Havvy : >> ({}) + ({})
ecmabot : Havvy: (string) '[object Object][object Object]'
DDR_ : The second way is the correct (sob) way.
ckknight : >> {} + ({})
ecmabot : ckknight: (number) NaN
ckknight : >> ({}) + {}
ecmabot : ckknight: (string) '[object Object][object Object]'
ckknight : qwies
ckknight : weird*
Benjie : ^_^
DDR_ : {} is undefined.
Havvy : >> {}
ecmabot : Havvy: undefined
DDR_ : In the console.
DDR_ : Heh, yes.
Havvy : >> undefined + {}
ecmabot : Havvy: (string) 'undefined[object Object]'
ckknight : DDR_: as an expression, yes
Havvy : >> undefined + undefined
ecmabot : Havvy: (number) NaN
ckknight : err
ckknight : as a statement, yes
ckknight : oh
ckknight : I get it
DDR_ : I'm guessing it's ambiguous or something.
DDR_ : I don't. :P
ckknight : {} + {} turns into {}; (+{})
Havvy : {}; (+{})
Havvy : >> {}; (+{})
ecmabot : Havvy: (number) NaN
ckknight : >> {} + {}
ecmabot : ckknight: (number) NaN
Havvy : ckknight: Yeah, that could very well be it.
ckknight : since the first is interpreted as a block statement
ckknight : and then you have a unary + and an object
DDR_ : Ah.
Benjie : ckknight: so the first {} is interpretted as a block
> statement. Fascinating, I'm impressed you reached that conclusion so
> quickly!