为什么在数组迭代中使用“for...in”不是一个好主意?

2073

我被告知在JavaScript中不要使用for...in循环遍历数组。为什么?


53
我看到最近有人对你说了这句话,但是他们只是针对数组而言。在遍历数组时进行迭代被认为是一种不好的做法,但在遍历对象成员时并不一定如此。 - mmurch
22
很多答案中使用了“for”循环,例如'for (var i=0; i<hColl.length; i++) {}',与'var i=hColl.length; while (i--) {}'相比较,当可以使用后者时,它的速度要快得多。我知道这可能离题了,但还是想加上这一点。 - Mark Schultheiss
3
@MarkSchultheiss 但那是反向迭代。 有没有另一种更快的正向迭代版本? - ma11hew28
5
@Wynand 使用 var i = hCol1.length; for (i;i;i--;) {},将 i 缓存起来会有所不同,并简化测试。对于老版本的浏览器而言,forwhile 的差异越大,因此请始终缓存 "i" 计数器。当然,负数并不总是适用于所有情况,并且 while 在某些人看来会使代码更加难懂。另外,请注意 var i = 1000; for (i; i; i--) {}var b =1000 for (b; b--;) {} 这两种写法,其中 i 从 1000 到 1,b 从 999 到 0。对于老版本的浏览器而言,while 倾向于使用 for 以提高性能。 - Mark Schultheiss
12
“你也可以聪明一些”。 for(var i = 0, l = myArray.length; i < l; ++i) ... 是使用顺序迭代时最快和最好的选择。 - Mathieu Amiot
显示剩余2条评论
29个回答

1731

原因是这个结构:

var a = []; // Create a new empty array.
a[5] = 5;   // Perfectly legal JavaScript that resizes the array.

for (var i = 0; i < a.length; i++) {
    // Iterate over numeric indexes from 0 to 5, as everyone expects.
    console.log(a[i]);
}

/* Will display:
   undefined
   undefined
   undefined
   undefined
   undefined
   5
*/

有时候,它可能与其他事物完全不同:

var a = [];
a[5] = 5;
for (var x in a) {
    // Shows only the explicitly set index of "5", and ignores 0-4
    console.log(x);
}

/* Will display:
   5
*/

还要考虑到JavaScript库可能会执行这样的操作,从而影响你创建的任何数组:

// Somewhere deep in your JavaScript library...
Array.prototype.foo = 1;

// Now you have no idea what the below code will do.
var a = [1, 2, 3, 4, 5];
for (var x in a){
    // Now foo is a part of EVERY array and 
    // will show up here as a value of 'x'.
    console.log(x);
}

/* Will display:
   0
   1
   2
   3
   4
   foo
*/


425
请使用(var x in a)而不是(x in a),以免创建全局变量。请注意保持原意,简化语言但不要添加解释或其他内容。 - Chris Morgan
94
第一个问题并不是让它变得糟糕的原因,只是语义上的差异。第二个问题对我来说是一个原因(除了库之间冲突外),改变内置数据类型的原型是不好的,而不是for..in本身不好。 - Stewart
91
在JavaScript中,所有对象都是关联数组。JS数组也是一个对象,因此它也是关联数组,但这不是它的主要用途。如果你想迭代一个对象的键,请使用for (var key in object)。但是,如果你想迭代一个数组的元素,请使用for(var i = 0; i < array.length; i += 1)。请注意,两种迭代方法的语法略有不同,并且用于不同类型的数据结构。 - Martijn
49
在第一个例子中,你说它“迭代从0到4的数字索引,正如每个人所期望的那样”。我期望它从0到5进行迭代!因为如果在位置5添加一个元素,数组将有6个元素(其中5个未定义)。 - stivlo
30
如果您将a[5]设为除5外的另一个值,比如a[5]=42,我认为这些例子会更加清晰。第二个例子(使用“for (x in a)”枚举)只枚举了一个值并不令我感到意外;但是它枚举的值是5而不是42,这对我来说是出乎意料的(因为其他类似枚举列表内容而不是数组索引的语言与此不同)。 - metamatt
显示剩余16条评论

430
for-in语句本身并不是"不良实践",但它可能被误用,例如用于枚举数组或类似数组的对象。 for-in语句的目的是枚举对象属性。该语句会沿着原型链向上遍历,还会枚举继承的属性,这有时并不是想要的。
此外,迭代顺序在规范中没有保证,这意味着如果你想要"迭代"一个数组对象,使用这个语句时不能确定属性(数组索引)将以数字顺序访问。
例如,在JScript (IE <= 8)中,即使是在数组对象上枚举的顺序也定义为创建属性的顺序:
var array = [];
array[2] = 'c';
array[1] = 'b';
array[0] = 'a';

for (var p in array) {
  //... p will be "2", "1" and "0" on IE
}

另外,谈到继承属性,如果你例如扩展了Array.prototype对象(像一些库如MooTools所做的那样),那些属性也将被枚举:

Array.prototype.last = function () { return this[this.length-1]; };

for (var p in []) { // an empty array
  // last will be enumerated
}

如我之前所说,遍历数组或类数组对象最好的方法是使用顺序循环,例如简单的 for/while 循环。

当您只想枚举对象的自有属性(即那些不是继承来的属性)时,可以使用 hasOwnProperty 方法:

for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    // prop is not inherited
  }
}

有些人甚至建议直接从Object.prototype调用该方法,以避免如果有人向我们的对象添加一个名为hasOwnProperty的属性而出现问题:

for (var prop in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, prop)) {
    // prop is not inherited
  }
}

10
请参考 David Humphrey 的文章 Iterating over Objects in JavaScript Quickly,对于数组而言,使用 for..in 循环比普通循环慢得多。 - Chris Morgan
18
关于“hasOwnProperty”的最后一点的问题:如果有人覆盖了一个对象上的“hasOwnProperty”,你会遇到问题。但是如果有人覆盖了“Object.prototype.hasOwnProperty”,那么你不是也会遇到同样的问题吗?无论哪种情况,他们都会破坏你的程序,而这不是你的责任,对吗? - Scott Rippey
你说for..in不是坏习惯,但它可能会被滥用。你有一个真实世界的好例子吗?在这个例子中,你确实想要遍历对象的所有属性,包括继承的属性? - rjmunro
4
@ScottRippey:如果你想看一下:https://www.youtube.com/watch?v=FrFUI591WhI - Nathan Wall
1
通过这个答案,我发现可以使用 for (var p in array) { array[p]; } 访问该值。 - equiman
此外,迭代顺序不能由规范保证。至少在ES2015之后,甚至可能在之前,这是不正确的。数组索引保证按升序数字顺序迭代。 - CertainPerformance

129

不应该使用for..in来迭代数组元素有三个原因:

  • for..in将循环遍历数组对象的所有自身属性和继承属性,但不包括DontEnum属性。这意味着如果有人向特定的数组对象添加属性(我自己也有这样做的合法原因)或更改了Array.prototype(这在代码与其他脚本协同工作时被认为是不好的实践),这些属性也将被迭代。通过检查hasOwnProperty()可以排除继承属性,但这不能帮助您处理在数组对象本身中设置的属性。

  • for..in不能保证元素的顺序不变

  • 它很慢,因为您必须遍历数组对象及其整个原型链的所有属性,并且仅会得到属性的名称,即需要进行额外的查找才能获取值


62

因为for...in枚举的是包含数组的对象,而不是数组本身。如果我向数组的原型链中添加一个函数,那么它也会被包括在内。例如:

Array.prototype.myOwnFunction = function() { alert(this); }
a = new Array();
a[0] = 'foo';
a[1] = 'bar';
for (var x in a) {
    document.write(x + ' = ' + a[x]);
}

这将会写入:

0 = foo
1 = bar
myOwnFunction = function() { alert(this); }

既然你永远无法确定原型链上是否会添加任何内容,因此最好使用for循环来枚举数组:

for (var i=0,x=a.length; i<x; i++) {
    document.write(i + ' = ' + a[i]);
}

这将会写入:

0 = foo
1 = bar

在这种情况下更好的陈述是:“不要在数组上使用for-in,因为它会包括您添加到原型中的任何内容”。 - Isaac Pak

57

截至2016年(ES6),我们可以使用 for...of 循环遍历数组,正如John Slegers已经注意到的那样。

我只想添加这个简单的演示代码,以使事情更清晰:

Array.prototype.foo = 1;
var arr = [];
arr[5] = "xyz";

console.log("for...of:");
var count = 0;
for (var item of arr) {
    console.log(count + ":", item);
    count++;
    }

console.log("for...in:");
count = 0;
for (var item in arr) {
    console.log(count + ":", item);
    count++;
    }

控制台显示:

for...of:

0: undefined
1: undefined
2: undefined
3: undefined
4: undefined
5: xyz

for...in:

0: 5
1: foo

换句话说:

  • for...of从0到5进行计数,并忽略Array.prototype.foo。它显示数组

  • for...in仅列出5,忽略未定义的数组索引,但添加了foo。它显示数组属性名称


46

简短回答:这并不值得。


较长的回答:即使不需要顺序元素顺序和最佳性能,这也不值得。


详细回答:这根本不值得……

  • 使用for (var property in array)会导致array作为一个对象被迭代,遍历对象原型链,最终比基于索引的for循环执行得慢。
  • for (... in ...)不能保证按预期的顺序返回对象属性。
  • 使用hasOwnProperty()!isNaN()检查过滤对象属性是一种额外的开销,导致它执行得更慢,并抵消了首先使用它的关键原因,即更简洁的格式。

由于这些原因,性能和方便之间的可接受折衷甚至不存在。除非意图将数组处理为对象并对数组的对象属性执行操作,否则没有任何好处。


41
在独立使用的情况下,使用for-in遍历数组是没有问题的。for-in循环遍历对象的属性名称,在“开箱即用”的数组中,属性对应于数组索引。(内置属性如lengthtoString等不包括在迭代中)。
但是,如果您的代码(或您正在使用的框架)向数组或数组原型添加自定义属性,则这些属性将被包含在迭代中,这可能不是您想要的。
一些JS框架,如Prototype修改了Array原型,而JQuery之类的其他框架则没有,因此可以安全地使用for-in。
如果你有疑问,可能就不应该使用for-in。
另一种迭代数组的替代方法是使用for循环:
for (var ix=0;ix<arr.length;ix++) alert(ix);

然而,这里存在一个不同的问题。问题在于JavaScript数组中可能存在“空洞”。如果您将arr定义为:

var arr = ["hello"];
arr[100] = "goodbye";

数组有两个元素,但长度为101。使用 for-in 循环会产生两个索引,而 for 循环将产生101个索引,其中99的值为undefined


35

除了其他回答中给出的原因,如果您需要对计数器变量进行数学运算,可能不希望使用“for...in”结构,因为循环遍历对象属性的名称,所以变量是字符串。

例如:

for (var i=0; i<a.length; i++) {
    document.write(i + ', ' + typeof i + ', ' + i+1);
}

将要编写

0, number, 1
1, number, 2
...

相比之下,

for (var ii in a) {
    document.write(i + ', ' + typeof i + ', ' + i+1);
}

会写

0, string, 01
1, string, 11
...

当然,这可以很容易地克服,只需包含相应的内容即可。

ii = parseInt(ii);

在循环中,但第一种结构更直接。


6
除非你真正需要整数或者忽略无效字符,否则你可以使用前缀 + 代替 parseInt - Konrad Borowski
另外,不建议使用 parseInt()。尝试一下 parseInt("025");,它会失败的。 - Derek 朕會功夫
7
你可以绝对使用 parseInt。问题在于,如果你不包含基数(radix),旧版浏览器可能会尝试解释数字(因此025变成八进制)。这个问题在ECMAScript 5中得到了修复,但以“0x”开头的数字仍然会被解释为十六进制。为了保险起见,使用基数来指定数字,例如parseInt("025", 10) - 这指定了十进制。 - IAmTimCorey

23
除了 for...in 循环遍历所有可枚举属性(这与“所有数组元素”不同!)之外,参见http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf,第 5 版的第 12.6.4 节或第 7 版的第 13.7.5.15 节:

列举属性的机制和顺序未指定...

(强调是我的。)

这意味着如果浏览器愿意,它可以按照插入的顺序、数字顺序或字典顺序(其中“30”在“4”之前!请记住,所有对象键 - 因此所有数组索引 - 实际上都是字符串,所以这是完全合理的)遍历属性。如果实现对象作为哈希表,则可以按桶进行遍历。或者将任何内容添加到“反向”中。甚至浏览器可以随机迭代并符合 ECMA-262,只要它恰好访问每个属性一次。

实际上,大多数浏览器当前喜欢以大致相同的顺序迭代。但是没有任何东西表明它们必须这样做。这是实现特定的,如果发现另一种方法更有效,则可以随时更改。

无论哪种方式,for...in 都不带有顺序的含义。如果您关心顺序,请明确说明并使用带有索引的常规 for 循环。


19

主要有两个原因:

第一

正如其他人所说,你可能会得到不在你的数组中或从原型中继承的键。所以,如果让我们说,一个库向数组或对象的原型添加了一个属性:

Array.prototype.someProperty = true

你将会在每个数组的一部分中得到它:

for(var item in [1,2,3]){
  console.log(item) // will log 1,2,3 but also "someProperty"
}

您可以通过使用hasOwnProperty方法来解决这个问题:

var ary = [1,2,3];
for(var item in ary){
   if(ary.hasOwnProperty(item)){
      console.log(item) // will log only 1,2,3
   }
}

但这对于使用for-in循环迭代任何对象都是正确的。

通常,数组中项目的顺序很重要,但是for-in循环不一定会按照正确的顺序进行迭代,因为它将数组视为一个对象,这是JS中实现的方式,而不是作为一个数组。 这似乎是一件小事,但它确实会破坏应用程序并且难以调试。


2
Object.keys(a).forEach( function(item) { console.log(item) } ) 遍历自身属性键的数组,而不是从原型继承的属性键。 - Qwerty
2
是的,但是像for-in循环一样,它不一定按正确的索引顺序进行。而且,在不支持ES5的旧版浏览器上无法运行。 - Lior
您可以通过在脚本中插入特定代码来教授这些浏览器使用array.forEach。请参阅Polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach - Qwerty
当然,但是这样你就在操作原型,这并不总是一个好主意...而且,你还有顺序的问题... - Lior

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