6

我注意到本机 forEach 有时即使对于小型数组来说也太慢了。看这个例子:

var a = [], b = []; 
a[1234567] = 'foo'; 
b[10] = 'bar'; 

a.forEach(function(arg1, arg2) { console.log(arg1, arg2); }); //1
//vs 
b.forEach(function(arg1, arg2) { console.log(arg1, arg2); }); //2

在我的 Chromium(25.0.1364.160 Ubuntu 12.04)中,第 1 行和第 2 行的执行时间是不同的数量级。我知道a的长度等于 1234568,而b的长度等于 10。但是本机 forEach 实现是否如此幼稚?a和b都只包含一个元素如何解释这种行为?

4

3 回答 3

10

那是因为a' 的长度实际上是 1234568,所以你必须循环 1234568 个元素,因为你怎么能确定这些元素不存在呢?

var a = []
a[1234567] = 'foo'
console.log(a.length) // 1234568

因此,它循环了 1234566 和 1 'foo',而数组b只循环了 9 个“无”和 1 'bar'

foreach尝试阅读a[0]时,它意识到它不存在,因为aindex 没有任何内容0。因此,foreach这样想:

I'm going to see what a[0] is!
Oh no! It's not there!
I'm going to see what a[1] is!
Oh no! It's not there!
I'm going to see what a[2] is!
Oh no! It's not there!
...
I'm going to see what a[1234567] is!
Yaaaaay! I found it! Now I'll print it!

这就是为什么它需要更长的时间。

于 2013-06-08T13:25:10.347 回答
5

forEach遍历数组的整个长度,在途中跳过不存在的元素。尽管a并且b仅包含一个元素,但它们length很大,因此您forEach很难进行迭代。

毕竟,它在规范中!ES5 15.4.4.18 Array.prototype.forEach的摘录:

6) 令 k 为 0。

7) 重复,而 k < len

为了证明这一点,让我们看看 Firefox 的 SpiderMonkey 是如何实现这些步骤的:

/* Steps 6-7. */
/* Steps a (implicit), and d. */
for (var k = 0; k < len; k++) {
    /* Step b */
    if (k in O) {
        /* Step c. */
        callFunction(callbackfn, T, O[k], k, O);
    }
}

您可以清楚地看到kfrom 0to lenwhich 的循环是您的性能问题的基础。几乎所有k的,k in O产量,false但你仍然感受到一百万次迭代和一百万k in O次测试的影响。

作为参考,这里是编写本文时SpiderMonkeyV8实现的链接。

于 2013-06-08T13:25:39.973 回答
4

但是 [is] 原生forEach实现 [so] 天真吗?a和b都只包含一个元素。[可以]如何解释这种行为[……]?

它在ECMAScript 语言规范 5.1 版第 15.4.4.18 节中指定

forEach使用一个或两个参数调用该方法时,将执行以下步骤:

  1. O为调用ToObject传递this值作为参数的结果。
  2. lenValue为使用参数调用O[[Get]]的内部方法的结果。"length"
  3. lenlenValueToUint32()
  4. 如果callbackfnfalse,则抛出异常。IsCallable()TypeError
  5. 如果提供了 thisArg,则令TthisArg;否则让Tundefined
  6. k为 0。
  7. 重复,而k < len

    一个。令PkkToString()

    湾。让kPresent成为使用参数Pk调用O[[HasProperty]]的内部方法的结果。

    C。如果kPresenttrue,则

    一世。令kValue为使用参数Pk调用O[[Get]]的内部方法的结果。

    ii. 使用T作为this值和包含kValuekO的参数列表调用 callbackfn[[Call]]内部方法。

    d。将k增加1。

  8. 返回未定义

步骤 6 和 7 需要一个一致的实现来遍历所有索引,从第一个索引0到最后一个索引a.length - 1无论是否存在具有该索引的数组元素。这就解释了为什么forEach如果最后一个索引是一个很大的数字,方法调用需要这么长时间。

但是,因为在步骤 7b(的实现)中,[[HasProperty]]内部方法必须为没有数组元素的索引返回false ,所以不会为这些索引调用回调callbackfn 。这就解释了为什么在执行方法调用时只有一个调用。console.logforEach

于 2013-06-08T14:02:25.180 回答