前言
之前看过一篇根据ECMA262规范讲解js this指向的问题的文章, 让我发现很多关于语言本身的问题, 如果用起来不符合咱程序员的直觉, 那就需要去看语言的设计规范了(毕竟语言都是人设计的, 设计者说怎样就得怎样). 所以我萌生了去读ECMA262 specification的想法。
当我读完两章之后, 觉得这个工程也太浩大了. 偏偏有"左耳进,右耳出"的功效, 遂放弃。但这个想法一直在心头萦绕.
今天看掘金这篇文章《源码拾遗系列(一):Axios》的时候, 遇到一段代码让我产生疑惑, 让我找准一个切入点, 去读ECMA262中一段关于Array.prototype.unshift的规范。
起因
在我看《源码拾遗系列(一):Axios》这篇文章时, 一段代码引起了我的注意。
// Get merged config
// Set config.method
// ...
var requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);// 引起我注意的代码
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
为什么这里要用Array.prototype.unshift.apply(chain, requestInterceptorChain);而不直接用chain.unshift(requestInterceptorChain)呢, 这两者的区别在哪里?
我打开console试了一试:
const chain = [1,2,3]
Array.prototype.unshift.apply(chain,[4,5,6])
console.log(chain) // 结果[4, 5, 6, 1, 2, 3]
而大家都知道如果直接使用chain.unshift([4,5,6])结果会是[[4,5,6],1,2,3]。
好违反直觉啊!
不过正好去调查一下, 看看规范能不能解决这个反直觉的疑惑。
调查开始
打开ECMA262规范的网站, ctrl + f 搜索 Array.prototype.unshift 导航到小节 23.1.3.31 Array.prototype.unshift ( ...items ):
(图片版,手机看不清)
(文字版, 排版不佳)
The arguments are prepended to the start of the array, such that their order within the array is the same as the order in which they appear in the argument list.
When the unshift method is called, the following steps are taken:
- Let O be ? ToObject(this value).
- Let len be ? LengthOfArrayLike(O).
- Let argCount be the number of elements in items.
- If argCount > 0, then a. If len + argCount > 2^53 - 1, throw a TypeError exception.
b. Let k be len.
c. Repeat, while k > 0,
i. Let from be ! ToString(𝔽(k - 1)).
ii. Let to be ! ToString(𝔽(k + argCount - 1)).
iii. Let fromPresent be ? HasProperty(O, from).
iv. If fromPresent is true, then
- Let fromValue be ? Get(O, from).
- Perform ? Set(O, to, fromValue, true). v. Else,
- Assert: fromPresent is false.
- Perform ? DeletePropertyOrThrow(O, to).
vi. Set k to k - 1.
d. Let j be +0𝔽.
e. For each element E of items, do
i. Perform ? Set(O, ! ToString(j), E, true).
ii. Set j to j + 1𝔽.
- Perform ? Set(O, "length", 𝔽(len + argCount), true).
- Return 𝔽(len + argCount).
我们以
const chain = [1,2,3]
Array.prototype.unshift.apply(chain,[4,5,6])
为例逐条来看:
The arguments are prepended to the start of the array, such that their order within the array is the same as the order in which they appear in the argument list.
When the unshift method is called, the following steps are taken:
【 参数会附加到array的起始处, 累加到array后的顺序和它们出现在参数列表里的顺序一致。 调用unshift时,会执行下面的步骤: 】
- Let O be ? ToObject(this value).
【 1. 令O(字母O)等于? ToObject(this value)的结果. 】
ToObject是规范里的抽象方法。 搜索ToObject是什么:
The abstract operation ToObject takes argument argument. It converts argument to a value of type Object according to Table 15:
ToObject是一个抽象方法,它根据参数的类型转化为相对应的值。
| Argument Type | Result |
|---|---|
| ... | ... |
| Object | Return argument. |
在Array.prototype.unshift.apply(chain,[4,5,6])中, this为chain, 所以调用ToObject(this value)等价于ToObject(chain)。 因chain是一个array, 属于object, 查看该表得结果为argument。 而此处argument就是chain, 故ToObject(chain)的结果为chain, 最终得到O等于chain。
- Let len be ? LengthOfArrayLike(O).
【 2. 令 len等于LengthOfArrayLike(O). 】
LengthOfArrayLike同样是规范里的抽象方法, 它会返回类数组(包括数字)对象的长度。
因为O是chain, 所以len = LengthOfArrayLike(O), len为3。
- Let argCount be the number of elements in items. 【 3. 令 argCount等于items的个数 】
还记得规范中对unshift方法的介绍吗: Array.prototype.unshift ( ...items )
在Array.prototype.unshift.apply(chain,[4,5,6])中,很显然[4,5,6]就是items。 那么argCount为3。
前3步更像是准备环境, 现在来到最关键第4步了, 我就直接将运算结果写到下面伪代码段里:
- If argCount > 0, then
【 经第3步得argCount为3, 走向a。】
a. If len + argCount > 2^53 - 1, throw a TypeError exception.
【 len + argCount 明显小于2^53 - 1, 走向b。】
b. Let k be len.
【 令 k 等于 len, 由第2步得len=3, 则k=3 】
c. Repeat, while k > 0,
【 只要k > 0, 重复下述步骤 】
i. Let from be ! ToString(𝔽(k - 1)).
【 令from为 ! ToString(𝔽(*k* - 1))]。 得from为 2 (暂时不深究这里的𝔽是什么) 】
ii. Let to be ! ToString(𝔽(k + argCount - 1)).
【 令 to为 ! ToString(𝔽(*k* + *argCount* - 1)。 得to=3+3-1为5 】
iii. Let fromPresent be ? HasProperty(O, from).
【 令 fromPresent 为 HasProperty(*O*, *from*)。 因O为chain=[1,2,3], from为2, 2确实为O的property, 得fromPresent为true 】
iv. If fromPresent is true, then
【 由上一步得fromPresent为true,继续then里的步骤 】
- Let fromValue be ? Get(O, from).
【 令 fromValue 为 ? Get(*O*, *from*)。 O为chain, from为2, 得fromValue = O['2']为3 】
- Perform ? Set(O, to, fromValue, true).
【 执行Set(*O*, *to*, *fromValue*, true)。 O为chain=[1,2,3], to为5, fromValue为3, 执行Set(O, to, fromValue, true) 即 O["5"]=3,得到 O = [1,2,3,,,3] 】
v. Else,
- Assert: fromPresent is false.
- Perform ? DeletePropertyOrThrow(O, to).
【 因from区间为[0, 2],O[from]一直有值,则fromPresent的值一直为true,所以不过经过这里Else的逻辑 】
vi. Set k to k - 1.
【 令k = k - 1,得k=2.再次重复上述的循环. 】
让我们来重复上述循环,此时:
k=2;
from 为 ! ToString(𝔽(k - 1)) 等于1;
to 为 ! ToString(𝔽(k + argCount - 1) 等于4;
fromPresent 为 HasProperty(O, from) 即O["1"]存在, 等于true;
fromValue 为 ? Get(O, from) 即O["1"]=2,等于2 。
执行 Perform ? Set(O, to, fromValue, true),即O[4]=2,得到O等于[1,2,3,,2,3] 此时k=1,
再次重复循环:
k=1;
from 为 ! ToString(𝔽(k - 1)) 等于0;
to 为 ! ToString(𝔽(k + argCount - 1) 等于3;
fromPresent 为 HasProperty(O, from) 即O["0"]存在, 等于true;
fromValue 为 ? Get(O, from) 即O["0"]=1,等于1 。
执行 Perform ? Set(O, to, fromValue, true),即O[3]=2,得到O等于[1,2,3,1,2,3] 此时k=0,
此时k不再大于0,则走向d.
d. Let j be +0𝔽.
【 令 j 为 0 】。
e. For each element E of items, do
【 遍历items的所有元素】( 由上文可知items为[4, 5, 6] ),
i. Perform ? Set(O, ! ToString(j), E, true).
【 执行 ? Set(*O*, ! ToString(j), *E*, true)】 ,则第一次的执行逻辑为O[0]=4
ii. Set j to j + 1𝔽.
【 令 j 为 j + 1 】
第一轮循环结束后,j=1。继续循环逻辑,可知O[1]=5,O[2]=6
对items的循环结束,得O为[4, 5, 6, 1, 2, 3]。
应该说此时,我们已经得到了我们想要的答案。
不过规范还剩最后两步:
5. Perform ? Set(O, "length", 𝔽(len + argCount), true).
【 执行 ? Set(O, "length", 𝔽(len + argCount), true) 】 即O.length = 3 + 3 = 6。 确实此时O为一个长度为6的数组。
- Return 𝔽(len + argCount).
【 返回len + argCount 为 6 】。
最后
通过对照规范一步一步执行,我们终于知道为什么执行Array.prototype.unshift.apply(chain,[4,5,6])会使chain值变成[4, 5, 6, 1, 2, 3]了。
那为什么chain.push([4,5,6])的结果却是[[4, 5, 6], 1, 2, 3]呢?
还记得apply方法是怎么用的吗?看看MDN对apply方法的定义:
func.apply(thisArg, [ argsArray])
apply方法接收两个参数,第一个参数为函数执行时的this,第二个参数为函数执行时所接收的参数数组。
所以,通过apply方法调用Array.prototype.unshift.apply(chain,[4,5,6])时,unshift接收到的参数其实可以写为unshift(4,5,6),可知传了三个参数,分别为4,5,6。而直接调用chain.unshift([4,5,6])时,只给unshift传了一个参数,为[4,5,6],
根据规范里unshift函数的定义,Array.prototype.unshift ( ...items )
按Array.prototype.unshift.apply(chain,[4,5,6])方式调用,items为[4,5,6]
按chain.unshift([4,5,6])方式调用,items为[[4,5,6]]是一个二位数组。
这也是为什么两种方式看似传了相同的参数,但最后的结果却不同了。
思考
如果规范里规定把无论多少维的items都转换成一维数组,那Array.prototype.unshift.apply(chain,[4,5,6]),chain.unshift([4,5,6])两种方式的结果就是一样的了。害,都是写规范的人规定的。
==========================
本文或许存在纰漏,还望交流与指正。