Array.apply实际上在做什么

Nei*_*eil 33 javascript arrays constructor

在阅读了这个SO问题后,我仍然对Array.apply实际上在做什么感到困惑.请考虑以下代码段:

new Array(5).map(function(){
  return new Array(5);
});
Run Code Online (Sandbox Code Playgroud)

我希望这可以初始化一个包含5个未定义条目的数组,然后映射它们创建一个5x5的二维数组);

相反,我只是得到数组,好像它从未映射过:

[undefined, undefined, undefined, undefined, undefined]
Run Code Online (Sandbox Code Playgroud)

当我在Array.apply调用中将构造函数调用包装到数组时,然后映射它,它按预期工作:

Array.apply(null, new Array(5)).map(function(){
  return new Array(5);
});
Run Code Online (Sandbox Code Playgroud)

导致;

[[undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined]];
Run Code Online (Sandbox Code Playgroud)

这是怎么回事?Array.apply是另一种调用new Array()或Array.prototype.constructor的方法吗?还有其他情况会有利吗?另外,为什么我的第一个方法没有在我发送的地图上接收?

谢谢!-Neil

Dan*_*Tao 55

好问题!

Array构造函数(具有可在不使用new)时通过多于1个参数,创建包含传入作为其元素的参数的阵列.所以你可以这样做:

Array(1, 2, 3); // => [1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

您可能知道,Function.prototype.apply允许您以数组的形式为函数提供参数.所以调用Array.apply允许你这样做,这将是与上面的代码等效的功能:

Array.apply(null, [1, 2, 3]); // => [1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

现在,这就是为什么事情有点令人困惑.该.apply()方法的规范是以特殊方式处理稀疏数组.稀疏数组的"长度"大于实际插入的元素数.例如:

var arr = [];
arr[0] = 'foo';
arr[5] = 'bar';
Run Code Online (Sandbox Code Playgroud)

上面构造的数组将具有Array.prototype.map6 的属性,因为它在索引0处有一个元素,在索引5处有一个元素.但是,由于在这些索引之间没有插入任何元素,如果调用length它,您将看到映射函数不适用于缺少的元素:

// This should throw, right? Since elements 1 through 4 are undefined?
var lengths = arr.map(function(s) { return s.length; });

// Nope!
lengths; // => [3, , , , , 3]
Run Code Online (Sandbox Code Playgroud)

为什么我们在你的例子中看到这种行为map?你猜对了:因为数组构造函数在给定单个参数时会创建一个具有指定长度的稀疏数组.

所以这里的问题是,虽然new Array(5)(和其他方法map,例如Array.prototype)通过跳过缺失值而特别针对稀疏数组行为,但该forEach方法没有任何这样的特殊行为.

  • @KevinB:这几乎让我很难过,但是......是的。这与`arr[1] = undefined`的原因相同;*不*相当于“delete arr[1];”的功能 (3认同)
  • 因此,在一种情况下,未定义索引,而在另一种情况下,将索引定义为未定义? (2认同)
  • 值得一提的是,`Function.prototype.apply` 不仅可以分解数组,还可以分解“类似数组”的对象。这意味着这两个函数在功能上是等效的:`Array.apply(null, { length: 5 })` == `Array.apply(null, Array(5))`。您决定哪一个更具可读性 (2认同)

And*_*yGS 7

这是非常有趣的例子。丹涛的回答很好。但我想我可以做一些额外的解释。

在第一种情况下

新数组(5)

创建一个空对象,然后它传递 throw 函数并给定长度为 5。由于缺少任何其他参数,该对象将不会获得分配的条目。

// Array(5) [ <5 empty slots> ]
Run Code Online (Sandbox Code Playgroud)

当您尝试“映射”这些条目时,由于缺少真实条目,实际上什么也没发生。

但是,如果您在此步骤之后尝试“array[0]”,例如,它会返回“undefined”...

在下一种情况下,您将在第一个“new Array(5)”之后使用 Array() 函数的“Call”方法(但实际上它没有区别“Call”或“Construct”调用方法与 Array 函数一起使用)。

Array.apply(null, new Array(5))

所以“new Array(5)”已经作为结果给出 Array(5) [ <5 empty slots> ] 和“Function.prototype.apply()”将这个数组分解为 Array() 函数传入的五个参数。在我们得到的当前步骤:

// Array(5) [ undefined, undefined, undefined, undefined, undefined ]
Run Code Online (Sandbox Code Playgroud)

这是五个真实的条目。我们可以用它们做“map()”。但是你的结果有一个小错误,因为我们目前得到

Array.apply(null, new Array(5)).map(function() { return new Array(5); });

有点不同的结果

/*
[…]    
0: Array(5) [ <5 empty slots> ]    
1: Array(5) [ <5 empty slots> ]  ?  
2: Array(5) [ <5 empty slots> ] ?    
3: Array(5) [ <5 empty slots> ] ?    
4: Array(5) [ <5 empty slots> ]
*/
Run Code Online (Sandbox Code Playgroud)

为了更精确,要获得“五对五,未定义”的结果,我们几乎不需要升级您的代码

Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); });
Run Code Online (Sandbox Code Playgroud)

这将返回“五对五,未定义”数组。

/*
[…] 
0: Array(5) [ undefined, undefined, undefined, … ] 
1: Array(5) [ undefined, undefined, undefined, … ] 
2: Array(5) [ undefined, undefined, undefined, … ] 
3: Array(5) [ undefined, undefined, undefined, … ] 
4: Array(5) [ undefined, undefined, undefined, … ]
*/
Run Code Online (Sandbox Code Playgroud)

但我要说的是,不仅“Function.prototype.apply()”具有分解没有实际条目的数组的当前行为。我给你举个例子:

Array(...new Array(5)).map(() => Array(...new Array(5)));
Run Code Online (Sandbox Code Playgroud)

这实际上会给我们完全相同的结果 - 五对五未定义。

如果我们仔细观察:

  1. 在第一个操作中,new Array(5)Array() 函数返回一个空数组,但长度属性值为“5”,因为它在“构造”模式下运行,并且有一个参数 (5)。
  2. 第二个和第三个动作Array.apply() | Array(...) 首先将没有元素的 Array 传播到 5 个参数,然后将它们传递给 Array()。

这是因为分解数组的“apply()”或“...”行为。当它获得数组的长度时,它会自动将“空槽”转换为未定义的值。

来自 Ecma-262/6.0 的参考

( http://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply )

19.2.3.1 Function.prototype.apply
    1. If IsCallable(func) is false, throw a TypeError exception.
    2. If argArray is null or undefined, then Return Call(func, thisArg).
    3. Let argList be CreateListFromArrayLike(argArray).

    7.3.17 CreateListFromArrayLike (obj [, elementTypes] )     
        1. ReturnIfAbrupt(obj).
        2. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object).
        3. If Type(obj) is not Object, throw a TypeError exception.
        4. Let len be ToLength(Get(obj, "length")).
        5. ReturnIfAbrupt(len).
        6. Let list be an empty List.
        7. Let index be 0.
        8. Repeat while index < len
            a. Let indexName be ToString(index).
            b. Let next be Get(obj, indexName).
            c. ReturnIfAbrupt(next).
            d. If Type(next) is not an element of elementTypes, throw a TypeError exception.
            e. Append next as the last element of list.
            f. Set index to index + 1.
        9. Return list.
Run Code Online (Sandbox Code Playgroud)
  1. 在'8a'子句中,我们得到“0”、“1”... indexNames 将它们作为数组对象的参数名称传递到 '8b'(在我们的例子中只是数组,没有任何 'like') - array["0 "], 数组["1"]...
  2. 每个请求的元素值都返回“未定义”,然后在 8'e' 中它们连续附加到参数列表中。
  3. “CreateListFromArrayLike”在“9”子句中的列表表示中返回带有未定义的五个参数,该参数被传递给 Function.prototype.apply() 并将应用于 Array() (它看起来像

new Array(undefined,undefined,undefined,undefined,undefined))。

扩展运算符使用迭代协议代替,但实际上它们在这种情况下的行为是相似的。