最近在Quora上看到有人在问为什么下面的执行结果为true
?
个人觉得这个题目很有意思,跟ECMAScript底层实现原理有关系,值得趁机再次温故而知新,希望能把它给讲个简单明白。
如果你是第一次看到如下的表达式,可能首先会让你觉得一头雾水,无从下手。
所以,为了简单清楚起见,我们可以先把它分成二部分,然后逐个击破,如下所示:
首先,我们先来看看右侧部分+[]
。总的来说,它的运算步骤如下:
[]
赋予expr
临时变量expr
上调用GetValue
方法,它会返回它所指向的对象,在这里就是expr
ToNumber($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]
是如何一步一步求值的:
[]
优先级最高,所以会先执行右侧的Property Accessors,即[[]][0]
Reference
的空数组,它的base
是[[]]
,而它的referenced name
是0
Prefix Increment Operator
,即++
操作我们假设[[]][0]
的执行结果为v
,那么++
操作的执行步骤如下:
v
赋予临时变量expr
expr
上调用GetValue
方法,它会返回它所指向的对象,即v
ToNumber($2)
,并将它赋予oldValue
oldValue
的基础上加1,即oldValue + 1
,并将结果赋予newValue
PutValue(v, newValue)
newValue
在这里,我们使用+
操作符来执行oldValue + 1
操作,默认它会首先将oldValue
转换为原生数据类型再执行+
操作。由于oldValue
的值为0
,本身已经是原生数据类型了,所以此处不会做类型转换操作。
现在我们已经得到了左侧和右侧的值,从而就简化为了如下表达式求值:
我们先说说+
操作符的运算步骤:
ToPrimitive
,且没有hint
值String
类型,则跳到第七步ToNumber
方法ToString
方法从而我们按照+
操作符的运算步骤进行计算,由于我们没有传入hint
值,所以hint
值默认为Number
(如果它是Date
类型,则为String
)。这也就意味着在[0].valueOf
方法会先调用,而不是toString
方法。由于[0].valueOf
方法返回的仍然是[0]
,它并不是原生数据类型,所以接着调用[0].toString
方法,得到结果'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
类型,而[]
不是,则抛异常。