JavaScript 中{}+{}是多少? (原创翻译)
写于:2018-04-10
原文:What is {} + {} in JavaScript? > Gary Bernhardt 最近在他的“Wat”演讲中,指出了一个 JavaScript 语言有趣而又奇怪之处:当你把对象和数组相加时,会得到意想不到的结果。 本文将解释逐一进行解释。 JavaScript 的加法规则其实很简单:你只能使用数字和字符串进行相加,而所有其他数据类型都将被转换为其中一种。为了理解类型转换的原理,我们首先需要了解一些基础知识。 注意:本文中提到的章节(比如第 9.1 节)都出自 ECMA-262 语言规范(ECMAScript 5.1)。 让我们先快速的复习一下:JavaScript 中有两种类型的值:原始值(primitive)和对象(object)[1]。 原始值包括:
undefined
、null
、boolean
、number
和string
。其他所有类型的值都是对象,如:array
和function
等。
1.类型转换
加法运算符会触发三种类型转换:它将值转换为原始值、number
或string
。
1.1. 用 ToPrimitive() 将值转换为原始值
ToPrimitive()
的使用语法如下:
ToPrimitive(input, PreferredType?)
可选参数PreferredType
可以是Number
或String
类型。它仅表示期望的转换类型,而最终结果可以是任何原始类型值。假如PreferredType
是Number
,则将通过以下步骤完成input
值的类型转换(第 9.1 节):
- 如
input
是原始类型值,则按原样返回。 - 如并非原始类型值而是对象,则调用
obj.valueOf()
。如结果为原始类型值,则直接返回该值。 - 如返回的并非原始类型值,则调用
obj.toString()
。如结果为原始类型值,则直接返回该值。 - 如返回的并非原始类型值,则抛出
TypeError
错误。 如PreferredType
是String
,则将步骤 2 和步骤 3 对调。如并未给出PreferredType
,对于Date
类型实例该值默认为String
,而对于所有其他值该值默认为Number
。
1.2. 用 ToNumber() 将值转换为数字
下表解释了ToNumber()
如何将原始类型值转换为数字的(第 9.3 节):
参数|结果
---|---
undefined
|NaN
null
|+0
boolean|true
转换为 1,false
转换为+0
number|不用转换
string|转换字符串中的数字,例如:将"324"
转换为324
通过调用对象obj
的ToPrimitive(obj,Number)
方法,对于得到的(原始类型的)结果调用ToNumber()
将其转换为数字。
1.3. 用 ToString() 将值转换为字符串
下表解释了 ToString()如何将原始类型值转换为字符串(第 9.8 节):
参数|结果
---|---
undefined
|undefined
null
|null
boolean|"true"
或"false"
number|原数字的字符串书写方式,比如:"1.765"
string|不用转换
通过调用对象obj
的ToPrimitive(obj,String)
方法,对于得到的(原始类型的)结果调用ToString()
将其转换为字符串。
1.4. 实践
通过以下对象,你将看到引擎内部的转换过程:
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
2.加法
对于如下加法运算:
value1 + value2;
执行该表达式时,内部运算逻辑如下(第 11.6.1 节):
- 将两个参加运算的值转换为原始类型值(以下是数学表示法,而非 JavaScript 代码):
prim1 := ToPrimitive(value1)
prim2 := ToPrimitive(value2)
此处省略了PreferredType
,因此对于 Date 类型的值该值默认为String
,其他类型的值该值默认为Number
。
- 如果 prim1 或 prim2 有一个是字符串,则将另一个也转换为字符串,最终返回字符串拼接后的结果。
- 如果都不是字符串,则将 prim1 和 prim2 都转换为数字,并返回他们之和。
2.1. 符合预期结果
当您将两个数组相加时,结果符合我们的预期:
> [] + []
''
将[]转换为原始类型值,首先会调用valueOf()
方法,最终返回数组本身(this):
> var arr = [];
> arr.valueOf() === arr
true
由于结果并非原始类型值,接下来将调用toString()
方法,返回空字符串(原始类型值)。这样一来,[]+[]
的结果,其实就是两个空字符串拼接的值。
将数组和对象相加也符合我们的预期:
> [] + {}
'[object Object]'
说明:将空对象转换为字符串的结果如下:
> String({})
'[object Object]'
因此,上一个表达式的结果就应该是""
和"[object Object]"
的字符串拼接的值。
更多将对象转换为原始类型值的例子:
> 5 + new Number(7)
12
> 6 + { valueOf: function () { return 2 } }
8
> "abc" + { toString: function () { return "def" } }
'abcdef'
2.2. 非预期结果
如果+
的头一个运算值是空对象字面量(在 Firefox 控制台输出的结果):
> {} + {}
NaN
这是怎么回事儿?这是由于 JavaScript 将第一个{}
解析为空代码块并忽略了。因此,通过计算+ {}
(加号和第二个{}
)最终得到NaN
。 这里的加号并非二元加法运算符,而是一元运算符前缀,它将其运算值转换为数字,其方法与Number()
相同,例如:
> +"3.65"
3.65
以下表达式的结果都相同:
+{};
Number({});
Number({}.toString()); // {}.valueOf() 并非原始类型值
Number('[object Object]');
NaN;
为什么第一个{}
被解析为代码块? 原因是整个输入内容被解析成了一段语句,而以开头花括号的语句会被解析为代码块。 你也可以强制把输入内容解析为表达式,从而来修正计算结果:
> ({} + {})
'[object Object][object Object]'
另外,函数或方法的参数也会被解析为表达式:
> console.log({} + {})
[object Object][object Object]
经过前面的讲解,见到下面的计算结果,你应该不会感到惊讶:
> {} + []
0
同样,上述语句也被解析为代码块和+ []
。以下表达式的结果都相同:
+[];
Number([]);
Number([].toString()); // [].valueOf() 并非原始类型值
Number('');
0;
有趣的是,Node.js 的REPL
在解析类似的输入时,与 Firefox 和 Chrome(同 Node.js 一样使用 V8 引擎)的解析结果不同。以下输入被解析为表达式,结果符合我们预期:
> {} + {}
'[object Object][object Object]'
> {} + []
'[object Object]'
它好处在于,其结果更像是将输入作为console.log()
参数时所得到的结果。而非将输入用在程序语句中所得到的结果。
3.总结
大多数情况下,理解 JavaScript 中+
的工作原理并不难:您只能将数字或字符串相加。 对象将被转换为字符串(如另一运算值是字符串的话)或数字(如另一运算值并非字符串)。 如需合并数组,则需要使用以下方法:
> [1, 2].concat([3, 4])
[ 1, 2, 3, 4 ]
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 }
如您还想了解更多关于运算符的知识,推荐阅读这篇文章“JavaScript 中的假运算符重载”。