为什么要对 JavaScript 数组进行预分配?

18

Firebug把(new Array(N))表示为一个数组,其中有Nundefined。最近我遇到了这样的场景:一个大小已确定、元素全是undefined的数组与新构造的、大小相等的数组不同,我希望能够理解它们之间的区别。

假设你想生成一个包含0到1000之间随机整数的列表。

function kilorange() {
    return Math.floor(Math.random() * (1001));
}

no_random_numbers = (new Array(6)).map(kilorange);
my_random_numbers = [undefined, undefined, undefined,
                     undefined, undefined, undefined].map(kilorange);
我原本以为no_random_numbersmy_random_numbers是相同的,但它们不是。 no_random_numbers是另一个由undefined组成的数组,而my_random_numbers是一个包含六个随机整数的数组。此外,在kilorange中加入console.count语句后,我发现使用Array构造函数创建的数组从未调用我的函数。
这两个数组有何区别?为什么map(以及其他可迭代方法)不能将上述数组视为相同的呢?

看一下new Array(N)实际上是在做什么的规范。没有任何元素被设置,只有数组的length被设置为N。如果你从0N迭代数组,你会得到Nundefined,尽管这些值没有被设置。 - Felix Kling
1
@FelixKling 感谢下面Davin的评论,让我提前了解了一些。但这引出了一个标题式的问题:创建长度非零的空数组有什么好处? - kojiro
2
我曾经看到的唯一一个使用它的例子是 new Array(6).join("some text"),它会将 "some text" 重复6次。 - Tetaxa
这并没有什么好处,所以没有人这样做。但它在规范中,你不能只是放弃它而不破坏一些脚本。可能的优点是数组的长度不必每次设置新元素时都增加,尽管性能提升可能微不足道。 - Felix Kling
等等,什么鬼?@Tetaxa,为什么那个能行? - kojiro
@kojiro 我不确定,但我猜它在JavaScript规范中,这里有测试用例 http://code.google.com/p/v8/source/browse/branches/bleeding_edge/test/mjsunit/array-join.js?r=8922 - Tetaxa
6个回答

7

ES标准(15.4.4.19)定义了map的算法,从第8b步可以清楚地看出,由于您的数组实际上没有这些元素,因此它将返回一个长度为6的“空”数组。

正如其他人所提到的,这与js中的数组对象有关,它们(与其严格的C语言对应物不同)非常动态,可能是稀疏的(请参见15.4中的稀疏性测试算法)。


文档很棒!我发现15.4.2.2也很相关。如果我理解正确,new Array(len)等同于(new Array())后直接设置长度属性。但这有什么用途呢? - kojiro
例如,理论上,如果编译器可以确定数组不会超出规定长度添加元素,并且不访问其他属性,则可以使用更快的类C数组来进行优化,而不是实现普通对象(更类似于哈希映射)。@kojiro - davin

2
看看这个示例并运行它:http://jsfiddle.net/ArtPD/1/(它创建了两个数组,而不使用map来遍历它们,然后列出了每个数组的键/值)。
我认为(new Array(6))不分配“命名”属性(因此不会创建“1”:undefined,“2”:undefined...),而另一种形式[undefined, ... ]则会。
实际上,如果我使用for (var i in ... ),则两个输出结果为:
no_random_numbers


my_random_numbers
0 undefined
1 undefined
2 undefined
3 undefined
4 undefined
5 undefined

2

当你使用:

var a = new Array(N);

新数组中没有存储任何值,甚至索引“properties”也没有被创建。这就是为什么 map 对该数组不起作用的原因。

Firebug 这样做的事实是 Firebug 的一个 bug/feature。您应该记住它的控制台是一个 eval 包装器。在 Firebug 控制台中还有其他 bug/feature。

例如,在 Chrome 控制台中,您将看到上述的 a 数组为 []


1
不错的问题,好的答案。我对MDN中的map原型进行了一些尝试。如果像这样进行调整,map将适用于new Array([length])
Array.prototype.map = function(callback, thisArg) {
    var T, A, k;
    if (this == null) {
      throw new TypeError(" this is null or not defined");
    }
    var O = Object(this);
    var len = O.length >>> 0;
    if ({}.toString.call(callback) != "[object Function]") {
      throw new TypeError(callback + " is not a function");
    }
    if (thisArg) {
      T = thisArg;
    }
    A = new Array(len);
    k = 0;
    while(k < len) {
      var kValue, mappedValue;
      if (k in O || (O.length && !O[k])) {
//                  ^ added this
        kValue = O[ k ];
        mappedValue = callback.call(T, kValue, k, O);
        A[ k ] = mappedValue;
      }
      k++;
    }
    return A;
};

根据Casy Hopes的回答,您还可以创建一个mapx扩展,以便能够使用一些new Array([length])map一起使用。
Array.prototype.mapx = function(callback){
  return this.join(',').split(',').map(callback);
}
//usage
var no_random_numbers = new Array(6).mapx(kilorange);

这很有趣,但我不会称其为Array.map,因为显然(new Array(n)).map(fn)的当前无操作行为是正确的。不想用直观、不正确的行为误导任何人。 ;) - kojiro

0
回答标题问题(为什么要预设数组),我遇到的唯一用途是初始化一个Array(n)数组,以创建一个长度为n-1的字符串:
var x = Array(6).join('-'); // "-----"

0

为了回答为什么要预设数组的问题,你可以这样做,这样就不必进行太多的单独分配。分配内存需要一些时间,而且可能会触发垃圾回收运行,这可能需要更长的时间。

在典型的网页上,你不会注意到任何区别,但如果你正在做一些重要的事情,它可能会有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接