循环语句每次比较i和数组长度时会检查数组的长度吗?

37

我正在浏览网页,然后我发现了这个

var i, len;
for(i = 0, len = array.length; i < len; i++) {  
   //...
}

我的第一反应是:

  • 他为什么要那样做?(一定有更好的原因)
  • 值得吗?(我认为肯定值得,否则他不会这样做)

普通循环(不缓存长度的循环)每次都会检查array.length吗?


3
缓存长度在不编译代码的旧浏览器中可能更重要。如果我没记错的话,Chrome可以很好地优化掉这个问题。 - RightSaidFred
1
@RightSaidFred - Chrome无法对其进行优化(因为循环内的长度可能会改变),但它足够快,两种方式之间没有太大的速度差异。 - jfriend00
@jfriend00:因为它是编译代码,所以Chrome似乎可以知道在循环中是否修改了数组,并在不需要优化时进行优化。但我完全同意,当涉及到Chrome时,这基本上是一个无关紧要的问题。 - RightSaidFred
@RightSaidFred - 或许如果循环中没有函数调用,循环中没有带有访问器的属性,循环中没有调用数组上的方法等等... 但是,在JS中很难知道什么可能或可能不会间接修改数组的长度。我认为Chrome足够快,以至于这些类型的优化不像以前那样有很大的差异。 - jfriend00
@jfriend00:是的,我完全理解你的意思。我经常想知道Chrome能够跟踪代码并进行优化到什么程度。有几次我在Google Closure Compiler中运行代码进行高级优化时,我对它所展开的代码感到非常惊讶。不过我不确定这对于问题的解决有何帮助。 - RightSaidFred
6个回答

30

一个由三个部分组成的循环按照以下方式执行:

for (A; B; C)

A - Executed before the enumeration
B - condition to test
C - expression after each enumeration (so, not if B evaluated to false)
所以,如果使用for(var i=0; i<array.length; i++)这种方式构建数组,则在每次枚举时会检查数组的.length属性。为了微观优化,将数组长度存储在临时变量中是有效的(另请参见:JavaScript中循环遍历数组的最快方法是什么?)。
for (var i=0; i<array.length; i++) { ... }等效:
var i = 0;
while (i < array.length) {
    ...
    i++;
}

问题在于属性查找。为了计算表达式B,每次迭代都需要在对象/数组上进行一次查找,这比在本地范围内进行查找要慢。如果可以在本地范围内找到某些东西,那么它总是查找的最快方式,这就是为什么您会使用var foo =将长度存储在当前范围中的原因。 - jAndy
1
差异非常依赖于浏览器。在旧版浏览器中,缓存更加有效。要进行比较,请查看:http://jsperf.com/caching-array-length/4 - Rob W
@RobW:当然可以。新的 JavaScript 引擎将会针对对象属性查找进行大量优化。引擎将具有内部查找哈希表以访问属性。因此,这种微小的优化在旧式浏览器(即 IE<9)上将产生更大的影响。 - jAndy

13
这当然值得。因为,正如您所说,循环将每次计算数组长度。这将导致巨大的开销。在您的firebug或chrome dev工具中运行以下代码片段, 您会发现最优化后的方式要比未最优化的方式快得多。请注意:我尝试将此代码放在jsPerf上,但我现在无法访问该网站。我猜我尝试时它已经关闭了。

2
您应该包括Firefox和Chrome的版本。这是一个非常详细的JSPerf:http://jsperf.com/caching-array-length/4 - Rob W

1
我一直以为在JavaScript中,length只是数组对象的一个属性,由之前的数组操作(创建、添加、删除)预先计算或被用户覆盖,所以你只是查找一个变量?我必须承认,我只是因为缺少括号而假设了这一点,但看着MDN页面上array.length的说明,它似乎也是这样说的。
在那些length是方法或长度由标准库函数计算的语言中,你应该在运行循环之前预先计算长度,这样数组不会在每次迭代中都被计算,特别是对于大型数据集。即便如此,在现代高级语言(如Python)中,len()仍然只是返回数组对象的长度属性。
因此,除非我搞错了,复杂度只是O(1),从这个角度来看,即使变量比每次查找属性稍微快一点,也不值得在保护的for循环范围外创建/重用额外的变量的潜在麻烦。
然而,我怀疑在这种情况下,示例程序员选择这种方法的原因只是他们在另一种语言中养成的习惯,然后将其带到了JavaScript中。

0

是的,它每次都会检查数组的长度,但如果我们不想为增加的数组运行循环,则可以使用forEach方法。

forEach是一个JavaScript内置方法,类似于for循环,但forEach仅遍历循环开始之前的数组元素。

让我们从以下片段中了解:

array = [1,2,3,4]

for(i=0;i<=array.length;i++){
console.log(array[i]);
array.push(array[i]+1);
}

输出如下: 1 2 3 4 5 6 ...等等(无限),同时每次都检查array.length

让我们使用forEach方法来检查

array = [1,2,3,4]
array.forEach((element) => {
console.log(element);
array.push(element+1);
})
console.log("array elements after loop",array);

它只处理在迭代开始之前存在于数组中的4个元素。

但是在forEach情况下,如果我们从数组中弹出元素,它将影响数组长度。

让我们通过一个例子来看看:

array = [1,2,3,4]
array.forEach((element) => {
console.log(element);
array.pop()
})
console.log("array elements after loop",array);


0
做这个的一个原因是,如果您在循环期间向数组添加元素但不想迭代它们。比如说,您想将[1, 2, 3]变成[1, 2, 3, 1, 2, 3]。您可以使用以下代码实现:
var initialLength = items.length;
for(var i=0; i<initialLength; ++i) {
    items.push(items[i]);
}

如果在循环之前不保存长度,那么数组的长度将会不断增加,直到浏览器崩溃/终止。

除此之外,正如其他人所说,这会轻微地影响性能。我不会因为“过早优化是万恶之源”而养成这种习惯。此外,如果您在循环期间更改数组的大小,则可能会破坏代码。例如,如果您在循环期间从数组中删除元素但仍然将 i 与先前的数组大小进行比较,则循环将尝试访问新大小之外的元素。


-1

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