137

While [] + [] is an empty string, [] + {} is "[object Object]", and {} + [] is 0. Why is {} + {} NaN?

> {} + {}
  NaN

My question isn't why ({} + {}).toString() is "[object Object][object Object]" while NaN.toString() is "NaN", this part has an answer here already.

My question is why does this happen only on the client side? On the server side (Node.js) {} + {} is "[object Object][object Object]".

> {} + {}
'[object Object][object Object]'

Summarizing:

On the client side:

 [] + []              // Returns ""
 [] + {}              // Returns "[object Object]"
 {} + []              // Returns 0
 {} + {}              // Returns NaN

 NaN.toString()       // Returns "NaN"
 ({} + {}).toString() // Returns "[object Object][object Object]"
 var a = {} + {};     // 'a' will be "[object Object][object Object]"

In Node.js:

 [] + []   // Returns "" (like on the client)
 [] + {}   // Returns "[object Object]" (like on the client)
 {} + []   // Returns "[object Object]" (not like on the client)
 {} + {}   // Returns "[object Object][object Object]" (not like on the client)
Community
  • 1
  • 1
Ionică Bizău
  • 109,027
  • 88
  • 289
  • 474
  • see also the spec: http://es5.github.io/ –  Jun 24 '13 at 05:22
  • 1
    @JanDvorak Thanks! They don't talk about NodeJS. – Ionică Bizău Jun 24 '13 at 05:28
  • 4
    It's just the browser console that does that. Try logging **to** the console and it's the same as in NodeJS. http://jsbin.com/oveyuj/1/edit – elclanrs Jun 24 '13 at 05:29
  • 2
    Not really a duplicate, I am asking for NodeJS answer. Voting for reopen... – Ionică Bizău Jun 24 '13 at 05:35
  • 4
    Hmm... sorry. However, http://stackoverflow.com/questions/9032856/what-is-the-explanation-for-these-bizarre-javascript-behaviours-mentioned-in-the is still relevant and answers the first half – John Dvorak Jun 24 '13 at 05:35
  • 3
    Don't forget that `{}` can be interpreted either as an expression or as an object primitive depending on context. Maybe the code is the same on client and on server but it is interpreting `{}` differently due to different context of entering the code. – Patashu Jun 24 '13 at 05:43
  • @elclanrs Not only in the browser. I've installed `rhino` in terminal that does the same thing (`{} + {}` is `NaN`). – Ionică Bizău Jun 24 '13 at 05:44
  • Like @Patashu said, it must be the context. Not sure what the subtle differences are in all these environments... – elclanrs Jun 24 '13 at 05:47
  • 2
    Concerning the node.js console, I'm guessing it's due to how the console is implemented. If the node.js console does `eval("x="+input)`, it will behave as observed – John Dvorak Jun 24 '13 at 05:48
  • @JanDvorak This should be an answer, the node console uses [this](http://nodejs.org/api/repl.html). – Benjamin Gruenbaum Jun 24 '13 at 06:32
  • 1
    @JanDvorak Also, it's more likely that it's because `new Function('return {}+{}')()` returns `"[object Object][object Object]"`. (At least according to the docs) – Benjamin Gruenbaum Jun 24 '13 at 06:33
  • 1
    @BenjaminGruenbaum are you willing to dig in the sources and then post an answer? – John Dvorak Jun 24 '13 at 06:35
  • 18
    Please reopen and then **stop closing this question again and again** since this question is really *not a duplicate*. – Alvin Wong Jun 24 '13 at 07:27
  • http://www.benjiegillam.com/2013/06/quantum-javascript/ answers this in more detail. – Mathias Bynens Jun 26 '13 at 11:28
  • @MathiasBynens Actually it's _less_ detail because it doesn't explain _why_ it evaluates this way in nodejs ("And I’m guessing that Node’s REPL,") :) Thanks for the link though. – Benjamin Gruenbaum Jun 26 '13 at 14:31
  • 1
    @BenjaminGruenbaum Note that this isn’t a comment to your answer, which is excellent! – Mathias Bynens Jun 26 '13 at 15:09

1 Answers1

133

Updated note: this has been fixed in Chrome 49.

Very interesting question! Let's dig in.

The root cause

The root of the difference is in how Node.js evaluates these statements vs. how the Chrome development tools do.

What Node.js does

Node.js uses the repl module for this.

From the Node.js REPL source code:

self.eval(
    '(' + evalCmd + ')',
    self.context,
    'repl',
    function (e, ret) {
        if (e && !isSyntaxError(e))
            return finish(e);
        if (typeof ret === 'function' && /^[\r\n\s]*function/.test(evalCmd) || e) {
            // Now as statement without parens.
            self.eval(evalCmd, self.context, 'repl', finish);
        }
        else {
            finish(null, ret);
        }
    }
);

This acts just like running ({}+{}) in the Chrome developer tools, which also produces "[object Object][object Object]" as you'd expect.

What the chrome developer tools do

On the other hand Chrome dveloper tools does the following:

try {
    if (injectCommandLineAPI && inspectedWindow.console) {
        inspectedWindow.console._commandLineAPI = new CommandLineAPI(this._commandLineAPIImpl, isEvalOnCallFrame ? object : null);
        expression = "with ((window && window.console && window.console._commandLineAPI) || {}) {\n" + expression + "\n}";
    }
    var result = evalFunction.call(object, expression);
    if (objectGroup === "console")
        this._lastResult = result;
    return result;
}
finally {
    if (injectCommandLineAPI && inspectedWindow.console)
        delete inspectedWindow.console._commandLineAPI;
}

So basically, it performs a call on the object with the expression. The expression being:

with ((window && window.console && window.console._commandLineAPI) || {}) {
    {}+{};// <-- This is your code
}

So, as you can see, the expression is being evaluted directly, without the wrapping parenthesis.

Why Node.js acts differently

Node.js's source justifies this:

// This catches '{a : 1}' properly.

Node did not always act like this. Here is the actual commit that changed it. Ryan left the following comment on the change: "Improve how REPL commands are evaled" with an example of the difference.


Rhino

Update - OP was interested in how Rhino behaves (and why it behaves like the Chrome devtools and unlike nodejs).

Rhino uses a completely different JS engine unlike the Chrome developer tools and Node.js's REPL which both use V8.

Here is the basic pipe line of what happens when you eval a JavaScript command with Rhino in the Rhino shell.

Basically:

Script script = cx.compileString(scriptText, "<command>", 1, null);
if (script != null) {
    script.exec(cx, getShellScope()); // <- just an eval
}

Out of the three, Rhino's shell is the one that does the closest thing to an actual eval without any wrapping. Rhino's is the closest to an actual eval() statement and you can expect it to behave exactly like eval would.

Roman
  • 4,922
  • 3
  • 22
  • 31
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    (Not really a part of the answer, but worth mentioning nodejs uses the [vm module](http://nodejs.org/docs/latest/api/vm.html) for evaling by default when using the REPL, and not just a JavaScript `eval`) – Benjamin Gruenbaum Jun 24 '13 at 06:43
  • Can you explain why **rhino**, for example, does the same thing in Terminal (not only Chrome Console)? – Ionică Bizău Jun 26 '13 at 06:10
  • 5
    +10 if it was possible! Wow man,... You really don't have life or you are really smarter than me to know something like that. Please, tell me that you searched a little bit to found this answer :) – Samuel Jun 28 '13 at 21:59
  • 7
    @Samuel All it took is reading the source - I swear! In Chrome, if you enter 'debugger;' , you get the whole pipe - it will throw you _directly_ to the 'with' with just one function above to `evaluateOn`. In node, everything is very well documented - they have a dedicated REPL module with all the history nice and cozy on git, having used REPLs before on my own programs, I knew where to look :) I'm glad you liked it and found it helpful, but I owe it to my familiarity with these code bases (dev-tools and nodejs) rather than my intellect. Going straight to the source is often always the easiest. – Benjamin Gruenbaum Jun 28 '13 at 22:08
  • 1
    Update - the console API in Chrome has been updated a bit so while the general idea here is correct, the code posted is not accurate for the most recent version of Chrome. See https://chromium.googlesource.com/chromium/blink.git/+/master/Source/core/inspector/InjectedScriptSource.js#688 – Benjamin Gruenbaum May 25 '14 at 23:37
  • Why does the with statement executed deep within the chrome developer tools force {} to be evaluated in a numeric context? If it was +{}+{} that'd convince me, except +{}+{} is 'Nan[object Object]' in both chrome developer tools and nodeJS – Paul Jun 01 '14 at 12:43
  • @Paul It gets executed as "Open a new code block, close it, then calculate `+{}`, Doing `+x` in JavaScript is exactly the same as doing `Number(x)` in JavaScript. So it gets executed as `Number({})` which is NaN – Benjamin Gruenbaum Jun 01 '14 at 12:58
  • @BenjaminGruenbaum I was the one who downvoted you because it didn't look good, but I upvoted it now because you updated your answer to be more detailed. –  Jun 02 '14 at 20:07
  • @İnekŞaban you didn't even have an account at the point of the last edit. what the hell are you on about? – rlemon Jun 02 '14 at 20:16
  • @rlemon I don't know if you read my post on Meta, but I have the habit to switch to a new account and ip address regularly. But I just wanted to come back to see if it was better, hence the upvote. –  Jun 02 '14 at 20:19
  • I presume your old account was deleted, so the negative vote would have been removed. – rlemon Jun 02 '14 at 20:27
  • @rlemon I'm here since late 2009 and it sometimes happen that an account get stuck in a ban (I presume because I share this connection with many people [it's a public hotspot]) and I make a new account thereof. But I mostly just switch for personal reasons. –  Jun 02 '14 at 20:59
  • @İnekŞaban link to your old account? – Benjamin Gruenbaum Jun 02 '14 at 21:07
  • @BenjaminGruenbaum What do you want to do with it? It's here anyway http://stackoverflow.com/users/107560/caglar-toklu If you delete it you won't resolve nothing, so please keep on a constructive standpoint. I don't want troubles and you don't want too. –  Jun 02 '14 at 21:25