JavaScript:原生forEach vs native forEach

时间:2013-06-08 13:21:22

标签: javascript performance foreach

我注意到,即使对于小型阵列,原生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 都只包含一个元素。如何解释这种行为?

3 个答案:

答案 0 :(得分:10)

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

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

所以,它正在循环超过1234566的任何内容和1 'foo',而不是数组b,它只循环超过9“没有”和'bar'

foreach尝试阅读a[0]时,它意识到它不存在,因为a在索引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!

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

答案 1 :(得分:5)

forEach遍历数组的整个长度,在路上跳过不存在的元素。虽然ab只包含一个元素,但他们的length很大,因此您需要花费forEach进行迭代。

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

  

6)设k为0。

     

7)重复,而k <1。 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);
    }
}

您可以清楚地看到k0len的循环,这是您性能问题的基础。对于几乎所有kk in O产生false,您仍然会感受到百万次迭代和一百万次k in O测试的影响。

供参考,以下是撰写本文时SpiderMonkeyV8实施的链接。

答案 2 :(得分:4)

  

但[原生] forEach实现[那么]天真? a b [仅包含]一个元素。如何解释这种行为?

ECMAScript Language Specification, 5.1 Edition, §15.4.4.18

中指定
  

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

     
      
  1. O 成为调用ToObject传递 this 值作为参数的结果。
  2.   
  3.   设 lenValue 是使用参数[[Get]]调用 O "length"内部方法的结果。
  4.   
  5. len 成为ToUint32 ( lenValue )
  6.   
  7. 如果IsCallable ( callbackfn ) false ,则抛出TypeError例外。
  8.   
  9. 如果提供了 thisArg ,请 T thisArg ;否则让 T 未定义
  10.   
  11. k 为0。
  12.   
  13. 重复,而 k &lt; len个

         
        

    一个。让 Pk ToString ( k )

             

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

             

    ℃。如果 kPresent 为真,那么

             
          

    我。设 kValue 是使用参数 Pk 调用 O [[Get]]内部方法的结果。

                 

    II。使用 T 调用 callbackfn [[Call]]内部方法作为值和包含 kValue 的参数列表, k O

        
             

    d。将 k 增加1。

      
  14.   
  15. 返回未定义
  16.   

步骤6和7要求符合要求的实现迭代所有索引,从第一个索引0到最后一个,a.length - 1,无论是否存在具有该索引的数组元素。这解释了如果最后一个索引是一个大数字,forEach方法调用需要这么长的原因。

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