理解这个“hack”需要理解几件事:
- 为什么我们不只是做
Array(5).map(...)
-
Function.prototype.apply 如何处理参数
-
Array 如何处理多个参数
-
Number 函数如何处理参数
-
Function.prototype.call 做了什么
它们是 javascript 中相当高级的主题,所以这将是相当长的。我们将从顶部开始。系好安全带!
1。为什么不只是Array(5).map?
什么是数组,真的吗?一个常规对象,包含整数键,映射到值。它还有其他特殊功能,例如神奇的length 变量,但在它的核心,它是一个常规的key => value 映射,就像任何其他对象一样。让我们玩一下数组,好吗?
var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined
//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']
我们得到了数组中的项目数 arr.length 与数组具有的 key=>value 映射数之间的内在差异,这可能与 arr.length 不同。
通过arr.length 扩展数组不会创建任何新的key=>value 映射,所以不是数组有未定义的值,它没有这些键 .当您尝试访问不存在的属性时会发生什么?你得到undefined。
现在我们可以稍微抬起头来,看看为什么像arr.map 这样的函数不会越过这些属性。如果 arr[3] 只是未定义,并且键存在,那么所有这些数组函数都会像任何其他值一样遍历它:
//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';
arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']
arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]
我故意使用方法调用来进一步证明密钥本身不存在这一点:调用undefined.toUpperCase 会引发错误,但事实并非如此。证明:
arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined
现在我们进入我的观点:Array(N) 是如何做事的。 Section 15.4.2.2 描述了这个过程。有一堆我们不关心的胡言乱语,但如果你设法在字里行间阅读(或者你可以相信我,但不要),它基本上可以归结为:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(在假设(在实际规范中检查)len 是有效的 uint32 而非任意数量的值的情况下运行)
所以现在你可以看到为什么 Array(5).map(...) 不起作用了 - 我们没有在数组上定义 len 项,我们没有创建 key => value 映射,我们只是改变了 length 属性.
现在我们已经解决了这个问题,让我们看看第二个神奇的东西:
2。 Function.prototype.apply 的工作原理
apply 所做的基本上是获取一个数组,并将其展开为函数调用的参数。这意味着以下内容几乎相同:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
现在,我们可以通过简单地记录 arguments 特殊变量来简化查看 apply 工作原理的过程:
function log () {
console.log(arguments);
}
log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
//["mary", "had", "a", "little", "lamb"]
//arguments is a pseudo-array itself, so we can use it as well
(function () {
log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
//["mary", "had", "a", "little", "lamb"]
//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
//[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]
//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!
log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]
在倒数第二个例子中很容易证明我的主张:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(是的,双关语)。 key => value 映射可能不存在于我们传递给apply 的数组中,但它肯定存在于arguments 变量中。这与上一个示例有效的原因相同:我们传递的对象上不存在键,但它们确实存在于arguments。
这是为什么呢?让我们看一下Section 15.3.4.3,其中定义了Function.prototype.apply。主要是我们不关心的事情,但有趣的部分是:
- 设 len 为使用参数“length”调用 argArray 的 [[Get]] 内部方法的结果。
这基本上意味着:argArray.length。然后规范继续对length 项执行简单的for 循环,生成对应值的list(list 是一些内部巫术,但它基本上是一个数组)。就非常非常松散的代码而言:
Function.prototype.apply = function (thisArg, argArray) {
var len = argArray.length,
argList = [];
for (var i = 0; i < len; i += 1) {
argList[i] = argArray[i];
}
//yeah...
superMagicalFunctionInvocation(this, thisArg, argList);
};
因此,在这种情况下,我们需要模仿argArray 的只是一个具有length 属性的对象。现在我们可以在arguments 上看到为什么值未定义但键未定义:我们创建了key=>value 映射。
唷,所以这可能不会比上一部分短。但是当我们完成时会有蛋糕,所以请耐心等待!然而,在接下来的部分(我保证会很短)之后,我们可以开始剖析表达式。如果您忘记了,问题是以下如何工作:
Array.apply(null, { length: 5 }).map(Number.call, Number);
3。 Array 如何处理多个参数
所以!我们看到了当您将 length 参数传递给 Array 时会发生什么,但在表达式中,我们传递了几个东西作为参数(准确地说是 5 个 undefined 的数组)。 Section 15.4.2.1 告诉我们该怎么做。最后一段对我们来说很重要,它的措辞真的很奇怪,但它可以归结为:
function Array () {
var ret = [];
ret.length = arguments.length;
for (var i = 0; i < arguments.length; i += 1) {
ret[i] = arguments[i];
}
return ret;
}
Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]
多田!我们得到一个包含几个未定义值的数组,并返回一个包含这些未定义值的数组。
表达式的第一部分
最后,我们可以破译以下内容:
Array.apply(null, { length: 5 })
我们看到它返回一个包含 5 个未定义值的数组,其中所有键都存在。
现在,到表达式的第二部分:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
这将是更简单、不复杂的部分,因为它不太依赖于晦涩难懂的技巧。
4。 Number 如何处理输入
执行Number(something) (section 15.7.1) 会将something 转换为数字,仅此而已。它是如何做到的有点令人费解,尤其是在字符串的情况下,但如果您感兴趣,该操作在section 9.3 中定义。
5。 Function.prototype.call的游戏
call 是apply 的兄弟,在section 15.3.4.4 中定义。它不接受参数数组,而是只接受它收到的参数,并将它们向前传递。
当您将多个call 链接在一起时,事情会变得有趣,将怪异的频率提高到 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
在您掌握发生的情况之前,这是非常值得的。 log.call 只是一个函数,相当于任何其他函数的 call 方法,因此,它本身也有一个 call 方法:
log.call === log.call.call; //true
log.call === Function.call; //true
call 是做什么的?它接受thisArg 和一堆参数,并调用它的父函数。我们可以通过apply 定义它(同样,非常松散的代码,行不通):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
让我们跟踪一下这是如何发生的:
log.call.call(log, {a:4}, {a:5});
this = log.call
thisArg = log
args = [{a:4}, {a:5}]
log.call.apply(log, [{a:4}, {a:5}])
log.call({a:4}, {a:5})
this = log
thisArg = {a:4}
args = [{a:5}]
log.apply({a:4}, [{a:5}])
后面的部分,或全部的.map
还没有结束。让我们看看当您为大多数数组方法提供函数时会发生什么:
function log () {
console.log(this, arguments);
}
var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^ ^-----------------------^
// this arguments
如果我们自己不提供this 参数,则默认为window。记下参数提供给我们的回调的顺序,让我们再次将其怪异到 11:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
哇哇哇...让我们后退一点。这里发生了什么?我们可以在section 15.4.4.18 中看到,其中定义了forEach,几乎会发生以下情况:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
所以,我们得到了这个:
log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);
现在我们可以看到.map(Number.call, Number) 是如何工作的:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
这会将当前索引i 转换为数字。
总之,
表达式
Array.apply(null, { length: 5 }).map(Number.call, Number);
分为两部分:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
第一部分创建一个包含 5 个未定义项的数组。第二个遍历该数组并获取其索引,从而产生一个元素索引数组:
[0, 1, 2, 3, 4]