What is {} + {} in JavaScript?

[2012-01-29] dev, javascript, jslang
(Ad, please don’t block)
Recently, the lightning talk “Wat” by Gary Bernhardt pointed out an interesting JavaScript quirk: You get unexpected results when you add objects and/or arrays. This post explains those results.

The general rule for addition in JavaScript is simple: You can only add numbers and strings, all other values will be converted to either one of those types. In order to understand how that conversion works, we first need to understand a few other things. Whenever a paragraph (such as §9.1) is mentioned, it refers to the ECMA-262 language standard (ECMAScript 5.1).

Let us start with a quick refresher. There are two kinds of values in JavaScript: primitives and objects [1]. The primitive values are: undefined, null, booleans, numbers, and strings. All other values are objects, including arrays and functions.

Converting values

The plus operator performs three kinds of conversion: It converts values to primitives, numbers and strings.

Converting values to primitives via ToPrimitive()

The internal operation ToPrimitive() has the following signature:
    ToPrimitive(input, PreferredType?)
The optional parameter PreferredType is either Number or String. It only expresses a preference, the result can always be any primitive value. If PreferredType is Number, the following steps are performed to convert a value input (§9.1):
  1. If input is primitive, return it as is.
  2. Otherwise, input is an object. Call obj.valueOf(). If the result is primitive, return it.
  3. Otherwise, call obj.toString(). If the result is a primitive, return it.
  4. Otherwise, throw a TypeError.
If PreferredType is String, steps 2 and 3 are swapped. If PreferredType is missing then it is set to String for instances of Date and to Number for all other values.

Converting values to numbers via ToNumber()

The following table explains how ToNumber() converts primitives to number (§9.3).

ArgumentResult
undefinedNaN
null+0
boolean valuetrue is converted to 1, false is converted to +0
number valueno conversion necessary
string valueparse the number in the string. For example, "324" is converted to 324

An object obj is converted to a number by calling ToPrimitive(obj, Number) and then applying ToNumber() to the (primitive) result.

Converting values to strings via ToString()

The following table explains how ToString() converts primitives to string (§9.8).

ArgumentResult
undefined"undefined"
null"null"
boolean valueeither "true" or "false"
number valuethe number as a string, e.g. "1.765"
string valueno conversion necessary

An object obj is converted to a number by calling ToPrimitive(obj, String) and then applying ToString() to the (primitive) result.

Trying it out

The following object allows you to observe the conversion process.
    var obj = {
        valueOf: function () {
            console.log("valueOf");
            return {}; // not a primitive
        },
        toString: function () {
            console.log("toString");
            return {}; // not a primitive
        }
    }
Number called as a function (as opposed to as a constructor) internally invokes ToNumber():
    > Number(obj)
    valueOf
    toString
    TypeError: Cannot convert object to primitive value

Addition

Given the following addition.
    value1 + value2
To evaluate this expression, the following steps are taken (§11.6.1):
  1. Convert both operands to primitives (mathematical notation, not JavaScript):
        prim1 := ToPrimitive(value1)
        prim2 := ToPrimitive(value2)
    
    PreferredType is omitted and thus Number for non-dates, String for dates.
  2. If either prim1 or prim2 is a string then convert both to strings and return the concatenation of the results.
  3. Otherwise, convert both prim1 and prim2 to numbers and return the sum of the results.

Expected results

When you are adding two arrays, everything works as expected:
    > [] + []
    ''
Converting [] to a primitive first tries valueOf() which returns the array itself (this):
    > var arr = [];
    > arr.valueOf() === arr
    true
As that result is not a primitive, toString() is called next and returns the empty string (which is a primitive). Therefore, the result of [] + [] is the concatenation of two empty strings.

Adding an array and an object also conforms to our expectations:

    > [] + {}
    '[object Object]'
Explanation: converting an empty object to string yields the following result.
    > String({})
    '[object Object]'
The previous result is thus created by concatenating "" and "[object Object]".

More examples where objects are converted to primitives:

    > 5 + new Number(7)
    12
    > 6 + { valueOf: function () { return 2 } }
    8
    > "abc" + { toString: function () { return "def" } }
    'abcdef'

Unexpected results

Things get weird if the first operand of + is an empty object literal (results as seen on the Firefox console):
    > {} + {}
    NaN
What is going on here? The problem is that JavaScript interprets the first {} as an empty code block and ignores it. The NaN is therefore computed by evaluating +{} (plus followed by the second {}). The plus you see here is not the binary addition operator, but a unary prefix operator that converts its operand to a number, in the same manner as Number(). For example:
    > +"3.65"
    3.65
The following expressions are all equivalent:
    +{}
    Number({})
    Number({}.toString())  // {}.valueOf() isn’t primitive
    Number("[object Object]")
    NaN
Why is the first {} interpreted as a code block? Because the complete input is parsed as a statement and curly braces at the beginning of a statement are interpreted as starting a code block. Hence, you can fix things by forcing the input to be parsed as an expression:
    > ({} + {})
    '[object Object][object Object]'
Arguments of functions or methods are also always parsed as expressions:
    > console.log({} + {})
    [object Object][object Object]
After the previous explanations, you should not be surprised about the following result, any more:
    > {} + []
    0
Again, this is interpreted as a code block followed by +[]. The following expressions are equivalent:
    +[]
    Number([])
    Number([].toString())  // [].valueOf() isn’t primitive
    Number("")
    0
Interestingly, the Node.js REPL parses its input differently from either Firefox or Chrome (which even uses the same V8 JavaScript engine as Node.js). The following input is parsed as expressions and the results are less surprising:
    > {} + {}
    '[object Object][object Object]'
    > {} + []
    '[object Object]'
This has the advantage of being more like the results you get when using the input as arguments of console.log(). But it is also less like using the input as statements in programs.

What does it all mean?

In most circumstances, it is not that difficult to understand how + works in JavaScript: You can only add numbers or strings. Objects are converted to either string (if the other operand is a string) or number (otherwise). If you want to concatenate arrays, you need to use a method:
    > [1, 2].concat([3, 4])
    [ 1, 2, 3, 4 ]
There is no built-in way in JavaScript to “concatenate” (merge) objects. You need to use a library such as Underscore:
    > var o1 = {eeny:1, meeny:2};
    > var o2 = {miny:3, moe: 4};
    > _.extend(o1, o2)
    { eeny: 1,
      meeny: 2,
      miny: 3,
      moe: 4 }
Note: In contrast to Array.prototype.concat(), extend() modifies its first argument:
    > o1
    { eeny: 1,
      meeny: 2,
      miny: 3,
      moe: 4 }
    > o2
    { miny: 3, moe: 4 }
If you are up for more fun with operators, you can read the post on “Fake operator overloading in JavaScript”.

References

  1. JavaScript values: not everything is an object