看到上面的表达式你可能会很懵,这是什么奇怪操作,为什么数组能相加,不要着急,听我细细道来。
本文章是对What is {} + {} in JavaScript?文章的翻译
在JavaScript中如果对数组和对象相加会得到意想不到的结果,本文章会推导这些计算是怎么得到的。
在JavaScript中,只做两种加法计算,一种是数字和数字的相加,一种是字符转和字符串相加。其余所有的加法都是将其他类型装换为这两种类型在做计算。为了弄明白这些转换是怎么实现的,我们先要搞懂一些基础知识。
我们快速复习下JavaScript中的类型:
-
简单类型(primitives)
- undefined
- null
- boolean
- number
- string
- symbol
-
复杂类型(对象类型)
- Function
- Object
- Map
- Set
- Array
1. 类型装换
加法运算符会触发三种类型转换:
- 转换为简单类型
- 转换为数字
- 转换为字符串
1.1 通过ToPrimitive进行类型转换
在JavaScript引擎内部有一个这样的操作符
ToPrimitive(input, PreferredType?)
可选参数PreferredType有两种类型,Number或String,虽然可以设置这个参数,但是转换结果并不一定是严格按这个参数装换(汗死),但是转换结果一定是一个简单类型primitives,如果PreferredType被标记为Number,这个操作符会按以下流程进行转换:
- 如果输入是一个简单类型,直接返回
- 否则,如果输入是一个对象,会调起这个对象的obj.valueOf() 方法,如果此方法返回的是简单类型,直接返回
- 否则,调用obj.toString() , 如果返回的是简单类型,直接返回
- 否则,报错
如果设置PreferredType为String,则上面二,三步调换执行顺序,如果过没有设置的话,PreferredType,类型将按一下规则装换:
- 如果input类型为Date,则将PreferredType设置为String
- 其他类型将作为Number处理
1.2 通过toNumber() 转换数字
数字转换规则如下:
| 参数 | 结果 |
|---|---|
| undefined | NAN |
| null | +0 |
| boolean | 如果true则转换为1,如果为false则转换为0 |
| number | 无需装换 |
| string | 将数字的部分转换为数字,如'123',转换为123 |
如果针对一个对象类型,先调用ToPrimitive(input, PreferredType?)操作符,将对象类型转成简单类型,在调用toNumber(),转换为数字。
1.3 通过toString() 转换为字符串
字符串转换规则如下:
| 参数 | 结果 |
|---|---|
| undefined | ‘undefined’ |
| null | ‘null’ |
| boolean | ‘true’ or ‘false’ |
| number | 将number转换为字符串,比如:123 -> '123' |
| string | 无需转换 |
如果针对一个对象类型,先调用ToPrimitive(input, PreferredType?)操作符,将对象类型转成简单类型,在调用toString(),转换为字符串。
1.4 Playground
var obj = {
valueOf: function () {
console.log("valueOf");
return {}; // not a primitive
},
toString: function () {
console.log("toString");
return {}; // not a primitive
}
}
Number(obj) 做为函数调用时,内部做如下转换
> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value
2. 加法
如下的加法操作:
value1 + value2
在计算此加法操作的时候,JavaScript引擎会进行如下步骤的操作:
- 将value1和value2转换为简单类型(此处表示伪代码,不是真是JavaScript代码)
prim1 := ToPrimitive(value1)
prim2 := ToPrimitive(value2)
// 因为PreferredType被省略,所以Date类型使用String,非Date类型使用Number
- 如果prim1,prim2,任意类型为String,则将另一个类型也转换为String,并将他们相加,返回
- 否则将prim1,prim2都转换为Number,相加,返回
2.1 情理之中
- 如果两个数组相加,正是我们预料的结果''
[] + []
// ''
[] 数组转换为简单类型:首先尝试调用valueOf() ,因为数组的valueOf() ,返回数组本身,如下:
const arr = [];
arr.valueOf() === arr; // true
因为数组的valueOf() ,返回不是简单类型,所以进而调用数组的toString() (而数组的toString() 是被重新过的,类似数组的join() ,方法),如果数组为空,toString() ,返回空串,因此 [] + [] 就等价于 '' + '' = ''。
- 数组和对象相加
[] + {}
// '[object Object]'
这是因为对象的toString() 方法,返回'[object Object]'
({}).toString(); // '[object Object]'
最终的结果就是'' + '[object Object]' = '[object Object]';
- 更多转换
> 5 + new Number(7)
12
> 6 + { valueOf: function () { return 2 } }
8
> "abc" + { toString: function () { return "def" } }
'abcdef'
2.2 意料之外
如果 +加法运算的第一个操作数是个空对象字面量,则会出现诡异的结果(Chrome console 中的运行结果):
{} + {}
NaN
看到上面的结果,你肯定会很奇怪,这是什么鬼,不应该是'[object Object]' + '[object Object]' = '[object Object] [object Object]'吗?为啥是NaN,好,我们在来看下面的例子:
{}.toString()
//Uncaught SyntaxError: Unexpected token '.'
上面的的表达式报错,这是为什么?
其实这个原因就在于空对象上,JavaScript将第一个空对象解释为空代码块,并且自动忽略它。NaN的计算结果是+{}的计算结果,这里的+并不是二元运算符,是一个一元远算符,就是将后面的表达式转换数字,和Number()的作用相同。例如:
> +"3.65"
3.65
下面表达式是等价的:
+{}
Number({})
Number({}.toString()) // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN
为什么第一个{}会被解析为代码块,因为整个输入被解析成了一个语句:如果左大括号出现在一条语句的开头,则这个左大括号会被解析成一个代码块的开始。可以通过将输入强制装换为表达式,如下:
({} + {})
// '[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]'
这样做的好处是更像是使用输入作为 console.log() 的参数时得到的结果。但这也不太像在程序中使用输入作为语句。
3 这是所有情况吗?
在大多数情况下,想要弄明白 JavaScript 中的 + 号是如何工作的并不难:你只能将数字和数字相加或者字符串和字符串相加。 对象值会被转换成原始值后再进行计算。单个数据我们理解,但是如果多个数组相加呢?
++[[]][+[]]+[+[]]
猜猜上面的表达式会得到什么?没错会得到10,下面我们就分析下:
(++[[]][+[]]) + ([+[]])
(++[[]][0]) + ([0])
(+([] + 1)) + [0]
1 + '0'
'10'
原文下面的情况太旧了就不翻译