++[[]][+[]]+[+[]] === '10'?

170 阅读6分钟

看到上面的表达式你可能会很懵,这是什么奇怪操作,为什么数组能相加,不要着急,听我细细道来。

本文章是对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有两种类型,NumberString,虽然可以设置这个参数,但是转换结果并不一定是严格按这个参数装换(汗死),但是转换结果一定是一个简单类型primitives,如果PreferredType被标记为Number,这个操作符会按以下流程进行转换:

  • 如果输入是一个简单类型,直接返回
  • 否则,如果输入是一个对象,会调起这个对象的obj.valueOf() 方法,如果此方法返回的是简单类型,直接返回
  • 否则,调用obj.toString() , 如果返回的是简单类型,直接返回
  • 否则,报错

如果设置PreferredTypeString,则上面二,三步调换执行顺序,如果过没有设置的话,PreferredType,类型将按一下规则装换:

  • 如果input类型为Date,则将PreferredType设置为String
  • 其他类型将作为Number处理

1.2 通过toNumber() 转换数字

数字转换规则如下:

参数结果
undefinedNAN
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'

原文下面的情况太旧了就不翻译