我在 es-discuss 邮件列表中遇到了以下代码:
Array.apply(null, { length: 5 }).map(Number.call, Number);
这产生
[0, 1, 2, 3, 4]
为什么这是代码的结果?这里发生了什么事?
我在 es-discuss 邮件列表中遇到了以下代码:
Array.apply(null, { length: 5 }).map(Number.call, Number);
这产生
[0, 1, 2, 3, 4]
为什么这是代码的结果?这里发生了什么事?
理解这个“hack”需要理解几件事:
Array(5).map(...)
Function.prototype.apply
处理参数Array
处理多个参数Number
函数如何处理参数Function.prototype.call
作用它们是 javascript 中相当高级的主题,所以这将是相当长的。我们将从顶部开始。系好安全带!
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
。
扩展数组 viaarr.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)
事情如何。第 15.4.2.2 节描述了该过程。有一堆我们不关心的笨蛋,但如果你设法读懂字里行间(或者你可以相信我,但不要),它基本上可以归结为:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(在假设(在实际规范中进行了检查)len
是有效的 uint32,而不仅仅是任意数量的值的假设下运行)
所以现在你可以明白为什么这样做Array(5).map(...)
行不通了——我们不在len
数组上定义项目,我们不创建key => value
映射,我们只是改变length
属性。
现在我们已经解决了这个问题,让我们看看第二件神奇的事情:
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
现在,我们可以apply
通过简单地记录arguments
特殊变量来简化查看工作原理的过程:
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
.
这是为什么?让我们看看第 15.3.4.3 节,其中Function.prototype.apply
定义了。大多数我们不关心的事情,但这里是有趣的部分:
- 令 len 为参数“length”调用 argArray 的 [[Get]] 内部方法的结果。
这基本上意味着:argArray.length
。然后规范继续对项目做一个简单的for
循环length
,生成一个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);
Array
处理多个参数所以!我们看到了当您将length
参数传递给 时会发生什么Array
,但是在表达式中,我们将一些东西作为参数传递(undefined
准确地说是 5 的数组)。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)
这将是更简单、不复杂的部分,因为它不太依赖于晦涩的黑客。
Number
对待输入做Number(something)
(第 15.7.1 节)会转换something
为数字,仅此而已。它是如何做到的有点令人费解,尤其是在字符串的情况下,但如果您感兴趣,该操作在第 9.3 节中定义。
Function.prototype.call
call
是apply
的兄弟,在第 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']]
// ^ ^
哇哇哇哇...让我们退后一点。这里发生了什么?我们可以在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]
免责声明:这是对上述代码的非常正式的描述 - 这就是我知道如何解释它的方式。要获得更简单的答案 - 请查看上面 Zirak 的精彩答案。这是您面对的更深入的规范,而不是“啊哈”。
这里正在发生几件事情。让我们把它分解一下。
var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
在第一行中,数组构造函数作为带有Function.prototype.apply
.
this
值null
对于 Array 构造函数无关紧要(this
与this
根据 15.3.4.3.2.a 的上下文中的相同。new Array
被称为传递一个具有length
属性的对象-.apply
由于以下子句 in ,这导致该对象成为一个数组,就像所有重要的一样.apply
:
.apply
被传递参数从0到.length
,由于主叫[[Get]]
上{ length: 5 }
具有值0〜4的产率undefined
的阵列构造函数被调用与五个参数,其值是undefined
(获得的对象的未声明属性)。var arr = Array.apply(null, { length: 5 });
创建了一个包含五个未定义值的列表。注意:注意这里Array.apply(0,{length: 5})
和之间的区别Array(5)
,第一个创建原始值类型的五倍,undefined
后者创建一个长度为 5 的空数组。具体来说,因为.map
的行为 (8.b)和具体[[HasProperty]
。
因此,符合规范的上述代码与以下代码相同:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
现在进入第二部分。
Array.prototype.map
Number.call
在数组的每个元素上调用回调函数(在本例中为)并使用指定的this
值(在本例中将this
值设置为 `Number)。Number.call
)是索引,第一个是 this 值。Number
使用this
as undefined
(数组值)和索引作为参数调用。所以它与将每个映射undefined
到其数组索引基本相同(因为调用Number
执行类型转换,在这种情况下从数字到数字不改变索引)。因此,上面的代码采用五个未定义的值并将每个值映射到它在数组中的索引。
这就是为什么我们将结果发送到我们的代码中。
正如你所说,第一部分:
var arr = Array.apply(null, { length: 5 });
创建一个包含 5 个undefined
值的数组。
第二部分是调用map
数组的函数,它接受 2 个参数并返回一个相同大小的新数组。
第一个参数map
实际上是一个应用于数组中每个元素的函数,它应该是一个接受 3 个参数并返回一个值的函数。例如:
function foo(a,b,c){
...
return ...
}
如果我们将函数 foo 作为第一个参数传递,它将为每个元素调用
需要的第二个参数map
被传递给您作为第一个参数传递的函数。但它不会是 a、b 或 c,在 的情况下foo
,它会是this
。
两个例子:
function bar(a,b,c){
return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]
function baz(a,b,c){
return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]
另一个只是为了让它更清楚:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
那么 Number.call 呢?
Number.call
是一个接受 2 个参数的函数,并尝试将第二个参数解析为一个数字(我不确定它对第一个参数的作用)。
由于map
传递的第二个参数是索引,因此将放置在该索引处的新数组中的值等于该索引。就像baz
上面例子中的函数一样。Number.call
将尝试解析索引 - 它自然会返回相同的值。
您map
在代码中传递给函数的第二个参数实际上对结果没有影响。如果我错了,请纠正我。
数组只是一个包含“长度”字段和一些方法(例如推送)的对象。所以 arr invar arr = { length: 5}
基本上与数组相同,其中字段 0..4 具有未定义的默认值(即arr[0] === undefined
产生真)。
至于第二部分,map,顾名思义,就是从一个数组映射到一个新的数组。它通过遍历原始数组并在每个项目上调用映射函数来实现。
剩下的就是让你相信 mapping-function 的结果是索引。诀窍是使用名为 'call'(*) 的方法调用一个函数,但有一个小例外,即第一个参数被设置为 'this' 上下文,第二个参数成为第一个参数(依此类推)。巧合的是,当调用映射函数时,第二个参数是索引。
最后但并非最不重要的是,调用的方法是数字“类”,正如我们在 JS 中所知,“类”只是一个函数,这个(数字)期望第一个参数是值。
(*) 在 Function 的原型中找到(而 Number 是一个函数)。
马沙尔