在 JavaScript 中创建范围-奇怪的语法

我在电子讨论邮件列表中遇到了以下代码:

Array.apply(null, { length: 5 }).map(Number.call, Number);

这就产生了

[0, 1, 2, 3, 4]

为什么这是代码的结果? 这里发生了什么?

15873 次浏览

免责声明 : 这是对上述代码的非常正式的描述——这是 知道如何解释它的方式。为了得到一个更简单的答案——检查上面 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 构造函数来说并不重要(根据15.3.4.3.2,this与上下文中的 this相同。一。
  • 然后 new Array被称为传递一个具有 length属性的对象——这会导致该对象成为一个数组,就像它对 .apply的影响一样,因为在 .apply中有以下子句:
    • 设 len 是使用参数“ length”调用 argArray 的[[ Get ]]内部方法的结果。
  • 因此,.apply将参数从0传递给 .length,因为在 { length: 5 }上用值0到4调用 [[Get]]将生成 undefined,数组构造函数将用值为 undefined的五个参数调用(获取对象的未声明属性)。
  • 数组构造函数 用0、2个或多个参数调用。 新构建的数组的 length 属性设置为根据规范的参数数目和相同值的值。
  • 因此,var arr = Array.apply(null, { length: 5 });创建一个包含五个未定义值的列表。

注意: 请注意 Array.apply(0,{length: 5})Array(5)之间的区别,前者创建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)。
  • Map 中回调的第二个参数(在本例中为 Number.call)是索引,第一个参数是 this 值。
  • 这意味着使用 this作为 undefined(数组值)和索引作为参数调用 Number。所以它基本上与将每个 undefined映射到它的数组索引相同(因为调用 Number执行类型转换,在本例中从 number 到 number 不改变索引)。

因此,上面的代码获取5个未定义的值,并将每个值映射到数组中的索引。

这就是为什么我们把结果输入到代码中。

如你所说,第一部分:

var arr = Array.apply(null, { length: 5 });

创建一个包含5个 undefined值的数组。

第二部分是调用数组的 map函数,该函数接受2个参数并返回一个大小相同的新数组。

map接受的第一个参数实际上是一个应用于数组中每个元素的函数,它应该是一个接受3个参数并返回一个值的函数。 例如:

function foo(a,b,c){
...
return ...
}

如果我们将 foo 函数作为第一个参数传递,那么每个元素都将使用

  • A 作为当前迭代元素的值
  • B 作为当前迭代元素的索引
  • 作为整个原始数组

map接受的第二个参数被传递给作为第一个参数传递的函数。但在 foo的情况下,它不是 a,b,c,它是 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是一个接受2个参数的函数,并尝试将第二个参数解析为一个数字(我不知道它对第一个参数做了什么)。

由于 map传递的第二个参数是索引,因此将放置在该索引处的新数组中的值等于索引。就像上面示例中的函数 baz一样。Number.call将尝试解析索引-它将自然地返回相同的值。

在代码中传递给 map函数的第二个参数实际上对结果没有影响。如果我说错了请纠正我。

数组仅仅是一个对象,包括“ length”字段和一些方法(例如 push)。所以 var arr = { length: 5}中的 arr 基本上与字段为0的数组相同。.4具有未定义的默认值(即 arr[0] === undefined为 true)。
至于第二部分,map,顾名思义,从一个数组映射到一个新数组。它通过遍历原始数组并在每个项上调用 map-function 来实现。

剩下的就是让你相信 map-function 的结果就是索引。技巧是使用名为‘ call’(*)的方法,该方法调用一个函数,但有一个小异常: 第一个参数设置为‘ this’上下文,第二个参数设置为第一个参数(以此类推)。巧合的是,当调用 map-function 时,第二个参数是索引。

最后但并非最不重要的,被调用的方法是数字“ Class”,正如我们在 JS 中知道的,“ Class”只是一个函数,而这个函数(Number)期望第一个参数是值。

(*)在 Function 的原型中找到(Number 是一个函数)。

马歇尔

理解这种“黑客行为”需要理解以下几点:

  1. 我们为什么不做 Array(5).map(...)
  2. Function.prototype.apply如何处理参数
  3. Array如何处理多个参数
  4. Number函数如何处理参数
  5. 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)是如何做事的。第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中。

为什么?让我们看看 第15.3.4.3节,其中定义了 Function.prototype.apply。大部分是我们不关心的事情,但有趣的是:

  1. 设 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的数组)。第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)(第15.7.1节)会将 something转换为一个数字,这就是全部。它是如何做到这一点是有点复杂的,特别是在字符串的情况下,但是操作是在 第9.3条中定义的,如果您感兴趣的话。

5. Function.prototype.call游戏

callapply的兄弟,在 第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']]
// ^    ^

喔,喔,喔... 我们倒回去一点。这是怎么回事?我们可以看到在定义 forEach第15.4.4.18节中,几乎发生了以下情况:

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]