js中加号(+)运算符的隐式转换

背景

1
2
3
4
5
6
7
8
9
console.log(1 + 2);		// 3
console.log('1' + '2'); // '12'
console.log(1 + '2'); // '12'
console.log(1 + undefined); // NaN
console.log(1 + null); // 1
console.log(1 + {}); // '1[object Object]'
console.log(1 + [2, 3, 4]); // '12,3,4'
console.log([] + []); // ''
console.log([] + {}); // '[object Object]'

神奇不?加法运算符(+)通常只用作数字相加以及字符串拼接,但在处理特殊值时有着特殊的行为

1 隐式转换的原理

1.1 JavaScript引擎内部的ToPrimitive()方法

1
ToPrimitive(input,PreferredType?)

抽象操作ToPrimitive将其输入参数转换为非对象类型。如果一个对象能够转换为多种原始类型,那么它可以使用可选的参数preferredType来支持该类型。

其中,可选参数PreferredType可以是Number或者String。 它只代表了一个转换的偏好,转换结果不一定必须是这个参数所指的类型,但转换结果一定是一个原始值(基本类型)。如果PreferredType被标志为Number,则会进行下面的操作来转换input

  • 第一步,如果input是个原始值,则直接返回它。
  • 第二步,如果input是一个引用类型。则调用obj.valueOf()方法。如果返回值是一个原始值,则返回这个原始值。
  • 第三步,调用obj.toString() 方法。 如果返回值是一个原始值,则返回这个原始值。
  • 都不是,抛出TypeError异常

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

  • Date 类型的对象会被设置为 String
  • 其它类型的值会被设置为 Number

1.2 JavaScript引擎内部的ToNumber()方法

ToNumber()方法将值转换为数字

1
ToPrimitive(obj, Number)

参数 结果
undefined NaN
null +0
Boolean true被转换为1,false转换为+0
Number 无需转换
String 由字符串解析为数字。例如,”123”被转换为123

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

1.3 JavaScript引擎内部的ToString()将值转换为字符串

ToNumber()方法将原始值转换成字符串

1
ToPrimitive(obj, String)

参数 结果
undefined “undefined”
null “null”
Boolean “true” 或者 “false”
Number 数字作为字符串。比如,”1.765”
String 无需转换

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

2 加法运算符

1
value1 + value2

在计算这个表达式时,内部的操作步骤是这样的

  • 1.将操作值value1、value2转换为原始值prim1、prim2。(因为省略了preferredType,因此Date类型是值采用String,其他类型的值采用Number);
  • 2.如果prim1或prim2中的任意一个为字符串,则将另外一个也转换成字符串,然后返回两个字符串连接操作后的结果。
  • 3.prim1和prim2都不是字符串,则数字运算求和。

ES5及以上可参考下列计算结果:

value1/value2 undefined Boolean Number String Function Object null Array
undefined Number Number Number String String String Number String
Boolean Number Number Number String String String Number String
Number Number Number Number String String String Number String
String String String String String String String String String
Function String String String String String String String String
Object String String String String String String String String
null Number Number Number String String String Number String
Array String String String String String String String String

3 其他

3.1 {} + {}

返回值为NaN,原因是,JavaScript 把第一个 {} 解释成了一个空的代码块(code block)并忽略了它。 NaN 其实是表达式 +{} 计算的结果 (+ 加号以及第二个 {})。 你在这里看到的+加号并不是二元运算符「加法」,而是一个一元运算符,作用是将它后面的操作数转换成数字,和 Number() 函数完全一样。

为什么第一个 {} 会被解析成代码块(code block)呢? 因为整个输入被解析成了一个语句:如果左大括号出现在一条语句的开头,则这个左大括号会被解析成一个代码块的开始。 所以,你也可以通过强制把输入解析成一个表达式来修复这样的计算结果: (译注:我们期待它是个表达式,结果却被解析成了语句)

1
2
console.log(({} + {}));	// '[object Object][object Object]'
console.log({} + []); // 0

Node.js 的 REPL 在解析类似的输入时,与 Firefox 和 Chrome(和Node.js 一样使用 V8 引擎) 的解析结果不同。

1
2
console.log(({} + {}));	// '[object Object][object Object]'
console.log({} + []); // '[object Object]'

3.2 特殊行为

1
+value
  • 某个运算数是NaN,那么结果为NaN
  • -Infinity-Infinity,结果为-Infinity
  • -Infinity-Infinity,结果为NaN
  • +0+0,结果为+0
  • -0+0,结果为+0
  • -0-0,结果为-0