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

2022-08-30 04:32:15

我在 es-discuss 邮件列表中遇到了以下代码:

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

这会产生

[0, 1, 2, 3, 4]

为什么这是代码的结果?这是怎么回事?


答案 1

了解这个“黑客”需要了解几件事:

  1. 为什么我们不只是做Array(5).map(...)
  2. 如何处理参数Function.prototype.apply
  3. 如何处理多个参数Array
  4. 函数如何处理参数Number
  5. 有什么作用Function.prototype.call

它们在javascript中是相当高级的主题,所以这将是相当长的。我们将从顶部开始。系好安全带!

1. 为什么不只是?Array(5).map

到底什么是数组?一个常规对象,包含映射到值的整数键。它还有其他特殊功能,例如神奇的变量,但在它的核心,它是一个常规的地图,就像任何其他对象一样。让我们玩一下数组,好吗?lengthkey => 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.lengthkey=>valuearr.length

通过 扩展数组不会创建任何新的映射,因此数组没有未定义的值,它没有这些键。当您尝试访问不存在的属性时会发生什么?你得到.arr.lengthkey=>valueundefined

现在我们可以抬起头来,看看为什么像这样的函数不会走过这些属性。如果只是未定义,并且键存在,则所有这些数组函数将像任何其他值一样对其进行检查:arr.maparr[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

现在我们到了我的观点:事情是怎么回事。第 15.4.2.2 节描述了该过程。有一堆我们不关心的mumbo jumbo,但是如果你设法在字里行间阅读(或者你可以相信我,但不要),它基本上可以归结为:Array(N)

function Array(len) {
    var ret = [];
    ret.length = len;
    return ret;
}

(在假设(在实际规范中检查)下操作,这是一个有效的uint32,而不仅仅是任何数量的值)len

所以现在你可以看到为什么做不起作用 - 我们不定义数组上的项目,我们不创建映射,我们只是改变属性。Array(5).map(...)lenkey => valuelength

现在我们已经解决了这个问题,让我们来看看第二个神奇的东西:

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

现在,我们可以通过简单地记录特殊变量来简化查看工作原理的过程:applyarguments

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 => valueapplyargumentsarguments

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

  1. 让 len 成为调用 argArray 的 [[Get]] 内部方法的结果,参数为“length”。

这基本上意味着:.然后,规范继续对项目进行简单的循环,使相应的值(是一些内部巫毒教,但它基本上是一个数组)。就非常非常松散的代码而言:argArray.lengthforlengthlistlist

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);
};

因此,在这种情况下,我们需要模仿的只是一个具有属性的对象。现在我们可以看到为什么值未定义,但键不是,在:我们创建映射。argArraylengthargumentskey=>value

哎呀,所以这可能不会比前一部分短。但是当我们完成时会有蛋糕,所以要有耐心!但是,在下一节之后(我保证这将是简短的),我们可以开始剖析表达式。如果您忘记了,问题是以下内容如何工作:

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

3. 如何处理多个参数Array

所以!我们看到了将参数传递给 时会发生什么,但是在表达式中,我们将几个东西作为参数传递(确切地说是 5 的数组)。第15.4.2.1节告诉我们该怎么做。最后一段对我们来说很重要,它的措辞非常奇怪,但它可以归结为:lengthArrayundefined

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

Doing(第 15.7.1 节)转换为一个数字,仅此而已。它是如何做到这一点的有点复杂,特别是在字符串的情况下,但是如果您感兴趣,该操作在9.3节中定义。Number(something)something

5. 游戏Function.prototype.call

call是 的兄弟,定义见第 15.3.4.4 节。它不是采用一组参数,而是只是获取它收到的参数,并将它们向前传递。apply

当你把多个链接在一起时,事情变得有趣,把奇怪的东西增加到11个:call

function log () {
    console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^  ^-----^
// this   arguments

这是相当wtf值得的,直到你掌握了发生了什么。 只是一个函数,等效于任何其他函数的方法,因此,它本身也有一个方法:log.callcallcall

log.call === log.call.call; //true
log.call === Function.call; //true

这又有什么作用呢?它接受一堆参数,并调用其父函数。我们可以通过以下方式定义它(再次,非常松散的代码,将不起作用):callthisArgapply

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

如果我们自己不提供参数,则默认为 .记下将参数提供给回调的顺序,让我们再次将其一直设置为 11:thiswindow

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]

答案 2

免责声明:这是对上述代码的非常正式的描述 - 这就是我知道如何解释它的方式。对于更简单的答案 - 查看上面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 的函数。

  • 该值对于 Array 构造函数无关紧要 ( 与根据 15.3.4.3.2.a 的上下文中的值相同。thisnullthisthis
  • 然后被称为传递一个带有属性的对象 - 这导致该对象成为一个数组,就像它的所有重要对象一样,因为以下子句:new Arraylength.apply.apply
    • 让 len 成为调用 argArray 的 [[Get]] 内部方法的结果,参数为“length”。
  • 因此,正在将参数从 0 传递到 ,因为使用值 0 到 4 调用会生成数组构造函数,该数组构造函数是使用五个参数调用的,其值为 (获取对象的未声明属性)。.apply.length[[Get]]{ length: 5 }undefinedundefined
  • 数组构造函数使用 0、2 个或更多参数调用。新构造数组的 length 属性根据规范设置为参数数,并将值设置为相同的值。
  • 因此,将创建包含五个未定义值的列表。var arr = Array.apply(null, { length: 5 });

注意:请注意 和 之间的区别,前者创建基元值类型的五倍,后者创建长度为 5 的空数组。具体来说,由于.map的行为(8.b),特别是.Array.apply(0,{length: 5})Array(5)undefined[[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)。]Number.callthisthis
  • map 中回调的第二个参数(在本例中为 )是索引,第一个是 this 值。Number.call
  • 这意味着使用 as(数组值)和索引作为参数来调用。因此,它基本上与将每个数组映射到其数组索引相同(因为调用Number执行类型转换,在这种情况下,从数字到数字不会更改索引)。Numberthisundefinedundefined

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

这就是为什么我们得到代码的结果。