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

IT技术 javascript ecmascript-5
2021-02-06 04:05:08

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

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

这产生

[0, 1, 2, 3, 4]

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

4个回答

理解这个“hack”需要理解几件事:

  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.lengthkey=>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属性。

现在我们已经解决了这个问题,让我们看看第二件神奇的事情:

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

现在,我们可以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定义了。大多数我们不关心的事情,但这里是有趣的部分:

  1. 令 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);

3. 如何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)

这将是更简单、不复杂的部分,因为它不太依赖于晦涩的黑客。

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']]
// ^    ^

哇哇哇哇...让我们退后一点。这里发生了什么?我们可以在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]
@Geek 我们只将一个参数传递给apply,但该参数被“拼凑”成两个传递给函数的参数。您可以在第一个apply示例中更轻松地看到这一点然后第一个console.log表明我们确实收到了两个参数(两个数组项),第二个console.log表明该数组key=>value在第一个插槽中有一个映射(如答案的第一部分所述)。
2021-03-19 04:05:08
2021-03-21 04:05:08
@Zirak 请帮助我理解以下内容ahaExclamationMark.apply(null, Array(2)); //2, true为什么它分别返回2true你不是只通过一个参数,即Array(2)在这里?
2021-03-24 04:05:08
请注意,将作为宿主对象的 NodeList 传递给本机方法log.apply(null, document.getElementsByTagName('script'));不是必需的,并且在某些浏览器中不起作用,并且[].slice.call(NodeList)将 NodeList 转换为数组在它们中也不起作用。
2021-04-05 04:05:08
一个更正:this仅默认Window为非严格模式。
2021-04-11 04:05:08

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

  • thisnull对于 Array 构造函数无关紧要(thisthis根据 15.3.4.3.2.a 的上下文中的相同
  • 然后new Array被称为传递一个具有length属性的对象-.apply由于以下子句 in ,这导致该对象成为一个数组,就像所有重要的一样.apply
    • 令 len 为参数“length”调用 argArray 的 [[Get]] 内部方法的结果。
  • 因此,.apply被传递参数从0到.length,由于主叫[[Get]]{ length: 5 }具有值0〜4的产率undefined的阵列构造函数被调用与五个参数,其值是undefined(获得的对象的未声明属性)。
  • 使用 0、2 或更多参数调用数组构造函数新构造的数组的长度属性根据规范设置为参数的数量,并将值设置为相同的值。
  • 因此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.mapNumber.call在数组的每个元素上调用回调函数(在本例中为)并使用指定的this值(在本例中将this设置为 `Number)。
  • map 中回调的第二个参数(在本例中Number.call)是索引,第一个是 this 值。
  • 这意味着Number使用thisas undefined(数组值)和索引作为参数调用。所以它与将每个映射undefined到其数组索引基本相同(因为调用Number执行类型转换,在这种情况下从数字到数字不改变索引)。

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

这就是为什么我们将结果发送到我们的代码中。

对于文档:地图如何工作的规范:es5.github.io/#x15.4.4.19,Mozilla 有一个示例脚本,该脚本根据developer.mozilla.org/en-US/docs/Web/JavaScript/上的规范运行参考/…
2021-03-13 04:05:08
@AndreasArray.apply(null,[2])就像Array(2)它创建了一个长度为 2数组,而不是一个包含undefined两次原始值的数组在第一部分之后的注释中查看我最近的编辑,让我知道它是否足够清楚,如果不是,我会对此进行澄清。
2021-04-05 04:05:08
我还没有理解它在第一次运行时的工作方式......第二次阅读后它是有道理的。{length: 2}伪造一个包含两个元素的数组,Array构造函数会将其插入到新创建的数组中。由于没有真正的数组访问不存在的元素undefined,然后将其插入。不错的技巧:)
2021-04-11 04:05:08
但是为什么它只适用于Array.apply(null, { length: 2 })而不是Array.apply(null, [2])它也会调用作为长度值Array传递构造2函数?小提琴
2021-04-12 04:05:08

正如你所说,第一部分:

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

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

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

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

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

如果我们将函数 foo 作为第一个参数传递,它将为每个元素调用

  • a 作为当前迭代元素的值
  • b 作为当前迭代元素的索引
  • c 作为整个原始数组

需要的第二个参数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在代码中传递给函数的第二个参数实际上对结果没有影响。如果我错了,请纠正我。

Number.call不是将参数解析为数字的特殊函数。它只是=== Function.prototype.call. 只有第二个参数,即作为this-value传递给 的函数call与 - 相关.map(eval.call, Number).map(String.call, Number)并且.map(Function.prototype.call, Number)都是等价的。
2021-03-28 04:05:08

数组只是一个包含“长度”字段和一些方法(例如推送)的对象。所以 arr invar arr = { length: 5}基本上与数组相同,其中字段 0..4 具有未定义的默认值(即arr[0] === undefined产生真)。
至于第二部分,map,顾名思义,就是从一个数组映射到一个新的数组。它通过遍历原始数组并在每个项目上调用映射函数来实现。

剩下的就是让你相信 mapping-function 的结果是索引。诀窍是使用名为 'call'(*) 的方法调用一个函数,但有一个小例外,即第一个参数被设置为 'this' 上下文,第二个参数成为第一个参数(依此类推)。巧合的是,当调用映射函数时,第二个参数是索引。

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

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

马沙尔

[undefined, undefined, undefined, …]new Array(n)or之间存在巨大差异{length: n}- 后者是sparse,即它们没有元素。这与 非常相关map,这就是使用奇数的原因Array.apply
2021-04-12 04:05:08