JavaScript中,{}+{} 等于多少?

原文:What is {} + {} in JavaScript?javascript

译者:justjavachtml


最近,Gary Bernhardt 在一个简短的演讲视频“Wat”中指出了一个有趣的 JavaScript 怪癖: 在把对象和数组混合相加时,会获得一些意想不到的结果。 本篇文章会依次讲解这些计算结果是如何得出的。java

在 JavaScript 中,加法的规则其实很简单,只有两种状况:express

  • 把数字和数字相加数组

  • 把字符串和字符串相加函数

全部其余类型的值都会被自动转换成这两种类型的值。 为了可以弄明白这种隐式转换是如何进行的,咱们首先须要搞懂一些基础知识。this

注意:在下面的文章中提到某一章节的时候(好比§9.1),指的都是 ECMA-262 语言规范(ECMAScript 5.1)中的章节。prototype

让咱们快速的复习一下。 在 JavaScript 中,一共有两种类型的值:翻译

  • 原始值(primitives)code

    1. undefined

    2. null

    3. boolean

    4. number

    5. string

  • 对象值(objects)。

除了原始值外,其余的全部值都是对象类型的值,包括数组(array)和函数(function)。

类型转换

加法运算符会触发三种类型转换:

  1. 转换为原始值

  2. 转换为数字

  3. 转换为字符串

经过 ToPrimitive() 将值转换为原始值

JavaScript 引擎内部的抽象操做 ToPrimitive() 有着这样的签名:

ToPrimitive(input,PreferredType?)

可选参数 PreferredType 能够是 Number 或者 String。 它只表明了一个转换的偏好,转换结果不必定必须是这个参数所指的类型(汗),但转换结果必定是一个原始值。 若是 PreferredType 被标志为 Number,则会进行下面的操做来转换 input (§9.1):

  1. 若是 input 是个原始值,则直接返回它。

  2. 不然,若是 input 是一个对象。则调用 obj.valueOf() 方法。 若是返回值是一个原始值,则返回这个原始值。

  3. 不然,调用 obj.toString() 方法。 若是返回值是一个原始值,则返回这个原始值。

  4. 不然,抛出 TypeError 异常。

若是 PreferredType 被标志为 String,则转换操做的第二步和第三步的顺序会调换。 若是没有 PreferredType 这个参数,则 PreferredType 的值会按照这样的规则来自动设置:

  • Date 类型的对象会被设置为 String

  • 其它类型的值会被设置为 Number

经过 ToNumber() 将值转换为数字

下面的表格解释了 ToNumber() 是如何将原始值转换成数字的 (§9.3)。

参数 结果
undefined NaN
null +0
boolean true被转换为1,false转换为+0
number 无需转换
string 由字符串解析为数字。例如,"324"被转换为324

若是输入的值是一个对象,则会首先会调用 ToPrimitive(obj, Number) 将该对象转换为原始值, 而后在调用 ToNumber() 将这个原始值转换为数字。

经过ToString()将值转换为字符串

下面的表格解释了 ToString() 是如何将原始值转换成字符串的(§9.8)。

参数 结果
undefined "undefined"
null "null"
boolean "true" 或者 "false"
number 数字做为字符串。好比,"1.765"
string 无需转换

若是输入的值是一个对象,则会首先会调用 ToPrimitive(obj, String) 将该对象转换为原始值, 而后再调用 ToString() 将这个原始值转换为字符串。

实践一下

下面的对象可让你看到引擎内部的转换过程。

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Number 做为一个函数被调用(而不是做为构造函数调用)时,会在引擎内部调用 ToNumber() 操做:

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

加法

有下面这样的一个加法操做。

value1 + value2

在计算这个表达式时,内部的操做步骤是这样的 (§11.6.1):

  1. 将两个操做数转换为原始值 (如下是数学表示法的伪代码,不是能够运行的 JavaScript 代码):

    prim1 := ToPrimitive(value1)
    prim2 := ToPrimitive(value2)

    PreferredType 被省略,所以 Date 类型的值采用 String,其余类型的值采用 Number

  2. 若是 prim1 或者 prim2 中的任意一个为字符串,则将另一个也转换成字符串,而后返回两个字符串链接操做后的结果。

  3. 不然,将 prim1 和 prim2 都转换为数字类型,返回他们的和。

预料到的结果

当你将两个数组相加时,结果正是咱们指望的:

> [] + []
''

[] 被转换成一个原始值:首先尝试 valueOf() 方法,该方法返回数组自己(this):

> var arr = [];
> arr.valueOf() === arr
true

此时结果不是原始值,因此再调用 toString() 方法,返回一个空字符串(string 是原始值)。 所以,[] + [] 的结果其实是两个空字符串的链接。

将一个数组和一个对象相加,结果依然符合咱们的指望:

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

解析:将空对象转换成字符串时,产生以下结果。

> String({})
'[object Object]'

因此最终的结果实际上是把 """[object Object]" 两个字符串链接起来。

更多的对象转换为原始值的例子:

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

意想不到的结果

若是 + 加法运算的第一个操做数是个空对象字面量,则会出现诡异的结果(Firefox console 中的运行结果):

> {} + {}
NaN

天哪!神马状况?(译注:原文没有,是我第一次读到这儿的时候感到太吃惊了,翻译的时候加入的。) 这个问题的缘由是,JavaScript 把第一个 {} 解释成了一个空的代码块(code block)并忽略了它。 NaN 实际上是表达式 +{} 计算的结果 (+ 加号以及第二个 {})。 你在这里看到的 + 加号并非二元运算符「加法」,而是一个一元运算符,做用是将它后面的操做数转换成数字,和 Number() 函数彻底同样。例如:

> +"3.65"
3.65

如下的表达式是它的等价形式:

+{}
Number({})
Number({}.toString())  // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN

为何第一个 {} 会被解析成代码块(code block)呢? 由于整个输入被解析成了一个语句:若是左大括号出如今一条语句的开头,则这个左大括号会被解析成一个代码块的开始。 因此,你也能够经过强制把输入解析成一个表达式来修复这样的计算结果: (译注:咱们期待它是个表达式,结果却被解析成了语句,表达式和语句的区别能够查看我之前的『代码之谜』系列的 语句与表达式。)

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

一个函数或方法的参数也会被解析成一个表达式:

> console.log({} + {})
[object Object][object Object]

通过前面的讲解,对于下面这样的计算结果,你也应该不会感到吃惊了:

> {} + []
0

在解释一次,上面的输入被解析成了一个代码块后跟一个表达式 +[]。 转换的步骤是这样的:

+[]
Number([])
Number([].toString())  // [].valueOf() isn’t primitive
Number("")
0

有趣的是,Node.js 的 REPL 在解析相似的输入时,与 Firefox 和 Chrome(和Node.js 同样使用 V8 引擎) 的解析结果不一样。 下面的输入会被解析成一个表达式,结果更符合咱们的预料:

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

3. 这就是全部吗?

在大多数状况下,想要弄明白 JavaScript 中的 + 号是如何工做的并不难:你只能将数字和数字相加或者字符串和字符串相加。 对象值会被转换成原始值后再进行计算。若是将多个数组相加,可能会出现你意料以外的结果,相关文章请参考在 javascript 中,为何 [1,2] + [3,4] 不等于 [1,2,3,4]?为何 ++[[]][+[]]+[+[]] = 10?

若是你想链接多个数组,须要使用数组的 concat 方法:

> [1, 2].concat([3, 4])
[1, 2, 3, 4]

JavaScript 中没有内置的方法来“链接" (合并)多个对象。 你可使用一个 JavaScript 库,好比 Underscore:

> var o1 = {eeny:1, meeny:2};
> var o2 = {miny:3, moe: 4};
> _.extend(o1, o2)
{eeny: 1, meeny: 2, miny: 3, moe: 4}

注意:和 Array.prototype.concat() 方法不一样,extend() 方法会修改它的第一个参数,而不是返回合并后的对象:

> o1
{eeny: 1, meeny: 2, miny: 3, moe: 4}
> o2
{miny: 3, moe: 4}

若是你想了解更多有趣的关于运算符的知识,你能够阅读一下 “Fake operator overloading in JavaScript”(中文正在翻译中)。

参考

  1. JavaScript 并不是全部的东西都是对象

  2. JavaScript:将全部值都转换成对象