我注意到,即使对于小型阵列,原生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 都只包含一个元素。如何解释这种行为?
答案 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
遍历数组的整个长度,在路上跳过不存在的元素。虽然a
和b
只包含一个元素,但他们的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);
}
}
您可以清楚地看到k
从0
到len
的循环,这是您性能问题的基础。对于几乎所有k
,k in O
产生false
,您仍然会感受到百万次迭代和一百万次k in O
测试的影响。
供参考,以下是撰写本文时SpiderMonkey和V8实施的链接。
答案 2 :(得分:4)
但[原生]
forEach
实现[那么]天真? a 和 b [仅包含]一个元素。如何解释这种行为?
在ECMAScript Language Specification, 5.1 Edition, §15.4.4.18:
中指定使用一个或两个参数调用
forEach
方法时,将执行以下步骤:
- 让 O 成为调用
ToObject
传递 this 值作为参数的结果。- 设 lenValue 是使用参数
[[Get]]
调用 O 的"length"
内部方法的结果。- 让 len 成为
ToUint32
(
lenValue)
。- 如果
IsCallable
(
callbackfn)
false ,则抛出TypeError
例外。- 如果提供了 thisArg ,请 T thisArg ;否则让 T 未定义。
- 让 k 为0。
重复,而 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。
- 返回未定义。
醇>
步骤6和7要求符合要求的实现迭代所有索引,从第一个索引0
到最后一个,a.length - 1
,,无论是否存在具有该索引的数组元素。这解释了如果最后一个索引是一个大数字,forEach
方法调用需要这么长的原因。
但是,因为在步骤7b(执行)中,[[HasProperty]]
内部方法必须为没有数组元素的索引返回 false ,所以回调 callbackfn 不为这些索引调用。这解释了为什么在执行console.log
方法调用时只有一个forEach
调用。