最近在Quora上看到有人在问为什么下面的执行结果为true?
++[[]][+[]]+[+[]] === '10'个人觉得这个题目很有意思,跟ECMAScript底层实现原理有关系,值得趁机再次温故而知新,希望能把它给讲个简单明白。
如果你是第一次看到如下的表达式,可能首先会让你觉得一头雾水,无从下手。
++[[]][+[]]+[+[]]所以,为了简单清楚起见,我们可以先把它分成二部分,然后逐个击破,如下所示:
(++[[]][+[]]) + ([+[]])
(++[[]][+[]]) // Left side
([+[]]) // Right side首先,我们先来看看右侧部分+[]。总的来说,它的运算步骤如下:
[]赋予expr临时变量expr上调用GetValue方法,它会返回它所指向的对象,在这里就是exprToNumber($2)ToNumber会调用ToPrimitive,并传入Number作为hint参数值,即ToPrimitive(input argument, hint Number)ToPrimitive会调用expr对象的内部方法[[DefaultValue]]并带上Number作为hint值,即[[DefaultValue]] (hint)hint为Number,那么[[DefaultValue]]首先会先调用expr对象上的valueOf方法,并将当前expr对象作为this值primValue临时变量,否则会继续调用expr对象上的toString方法,并将当前对象作为this值primValue临时变量,否则抛出TypeError异常ToNumber(primValue)并返回结果虽然上面步骤很多,但是一目了然,再清楚不过了。
但是,这里仍然需要对ToPrimitive这个核心函数多做一下解释。ToPrimitive主要是用于将任意类型的值转换为对应的原生类型的值。如果ToPrimitive函数的输入已经是原生类型值,那么会原样返回该值,不会做任何类型转换。但是,如果该输入值是非原生数据类型,那么它就会通过调用内部方法[[DefaultValue]]以便找到与该对象相对应的默认值。
其中,[[DefaultValue]]方法是每一个对象都有的一个内部方法。[[DefaultValue]]可以接受一个可选参数hint,hint必须是Number或是String。
如果可选参数
hint没有提供,那么它的默认值即是Number。但是这里有一个例外,如果对象是Date类型的,那么hint的默认值就会是String。
如果可选参数hint已经确定好了,那么[[DefaultValue]]会根据hint参数依次选择调用toString和valueOf,直到找到该对象的原生数据类型值为止,这里就是我们为什么需要hint的原因,因为它决定了调用这二个方法的顺序。如果hint为Number,那么valueOf方法会首先被调用,但是如果它是String,那么toString方法会首先被调用。运行过程如下:
callable,那么就执行它并返回它的执行结果,否则跳到第三步callable,那么就执行它并返回它的执行结果,否则跳到第五步TypeError异常由上我们可以看出,[[DefaultValue]]的返回结果必定会是原生数据类型,如若不是,则会抛出TypeError异常。
所以,对于+[]来说,首先会调用[].valueOf方法,它的返回仍然是[],由于它不是原生数据类型,所以会接着再调用[].toString,此时返回的是空空字符串'',之后再调用ToNumber(''),即+[]计算的第九步,最后返回运算结果0。
那么,到此为止,右侧部分[+[]]的结果就是[0]。
从前面对右侧表达式[+[]]的分析,我们已经知道了如下的结论:
[+[]] // [0]那么,++[[]][+[]]就可以进一步拆解为++[[]][0]。
我们再来看看++[[]][0]是如何一步一步求值的:
[]优先级最高,所以会先执行右侧的Property Accessors,即[[]][0]Reference的空数组,它的base是[[]],而它的referenced name是0Prefix Increment Operator,即++操作我们假设[[]][0]的执行结果为v,那么++操作的执行步骤如下:
v赋予临时变量exprexpr上调用GetValue方法,它会返回它所指向的对象,即vToNumber($2),并将它赋予oldValueoldValue的基础上加1,即oldValue + 1,并将结果赋予newValuePutValue(v, newValue)newValue在这里,我们使用+操作符来执行oldValue + 1操作,默认它会首先将oldValue转换为原生数据类型再执行+操作。由于oldValue的值为0,本身已经是原生数据类型了,所以此处不会做类型转换操作。
oldValue // 0
oldValue + 1 // 现在我们已经得到了左侧和右侧的值,从而就简化为了如下表达式求值:
1 + [0]我们先说说+操作符的运算步骤:
ToPrimitive,且没有hint值String类型,则跳到第七步ToNumber方法ToString方法从而我们按照+操作符的运算步骤进行计算,由于我们没有传入hint值,所以hint值默认为Number(如果它是Date类型,则为String)。这也就意味着在[0].valueOf方法会先调用,而不是toString方法。由于[0].valueOf方法返回的仍然是[0],它并不是原生数据类型,所以接着调用[0].toString方法,得到结果'0',那么就变成了这样:
1 + '0'由于0是String,则接着会执行字符串拼接操作,即先将+两侧的操作数转换为String,然后再返回两个字符串拼接的值,即10,至此,我们已经知道了++[[]][+[]]+[+[]] === '10'的整个求值过程。
++[[]][0]返回1,而++[]会抛ReferenceError首先,执行++[]时抛出的ReferenceError是在11.4.4 Prefix Increment Operator中的第五步中执行PutValue时抛出的。
执行PutValue(expr, newValue)时第一步就会去检测参数expr是否是Reference类型,如果不是,则会抛出ReferenceError。
知道了这点就可以很好解释了,因为从++[[]][0]执行步骤我们可以知道,[[]][0]其实是一种MemberExpression [ Expression ] ,它返回的是一个Reference类型,而[]不是,则抛异常。