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


我被告知不要使用 for...in 使用JavaScript中的数组。为什么不?


1540
2018-02-01 09:46


起源


我看到了最近有人对你这么说的问题,但它们只适用于阵列。迭代数组被认为是不好的做法,但不一定要迭代对象的成员。 - mmurch
许多带有“for”循环的答案,例如'for(var i = 0; i <hColl.length; i ++){}'与'var i = hColl.length; while(i - ){}',当可以使用后一种形式时,它实质上更快。我知道这是切向的,但我想我会加上这一点。 - Mark Schultheiss
@MarkSchultheiss,但这是反向迭代。还有另一种版本的前向迭代更快吗? - ma11hew28
@Wynand使用 var i = hCol1.length; for (i;i;i--;) {} 缓存i,因为它会产生影响,并简化测试。 - 浏览器越旧,之间的差异越大 for和 while 总是缓存“i”计数器 - 当然负面并不总是适合这种情况,而负面的则是负面的 obfuscate  有些人的代码。并注意 var i = 1000; for (i; i; i--) {}和 var b =1000 for (b; b--;) {} 其中我将1000转为1,b则转为999到0. - 浏览器越老,时间越有利于性能。 - Mark Schultheiss
你也可以聪明。 for(var i = 0, l = myArray.length; i < l; ++i) ... 是前向迭代可以获得的最快和最好的。 - Mathieu Amiot


答案:


原因是一个结构:

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
*/


1342
2018-02-01 10:08



从历史上看,有些浏览器甚至迭代过'length','toString'等等! - bobince
记得要用 (var x in a) 而不是 (x in a)  - 不想创建全局。 - Chris Morgan
第一个问题不是它不好的原因,只是语义上的差异。第二个问题在我看来是一个原因(除了库之间的冲突之外)改变内置数据类型的原型是坏的,而不是for..in是坏的。 - Stewart
@Stewart: 所有对象 在JS中是关联的。 JS数组是一个对象,所以是的,它也是关联的,但这不是它的用途。如果你想迭代一个对象 按键, 使用 for (var key in object)。如果你想迭代一个数组 分子然而,使用 for(var i = 0; i < array.length; i += 1)。 - Martijn
你说的第一个例子就是它 迭代从0到4的数字索引,正如大家所期望的那样, 一世 期望它从0迭代到5!因为如果在位置5中添加元素,则数组将具有6个元素(其中5个元素未定义)。 - stivlo


for-in 声明本身并不是一种“坏习惯”,但它可以 误用例如,到 迭代 在数组或类似数组的对象上。

的目的 for-in 声明是 枚举 对象属性。这个陈述将在原型链中出现,也列举过来 遗传 属性,一个东西 有时 是不希望的。

此外,规范不保证迭代的顺序,这意味着如果要“迭代”数组对象,则使用此语句无法确保将按数字顺序访问属性(数组索引)。

例如,在JScript(IE <= 8)中,即使在Array对象上的枚举顺序也定义为创建的属性:

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
}

正如我之前所说的那样 迭代 在数组或类似数组的对象上,最好的方法是使用a 顺序循环比如一个普通的老人 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
  }
}

356
2017-11-23 21:22



另见David Humphrey的帖子 快速迭代JavaScript中的对象  - 对于阵列 for..in 比“正常”循环要慢得多。 - Chris Morgan
关于“hasOwnProperty”的最后一点的问题:如果有人在对象上覆盖“hasOwnProperty”,那么你就会遇到问题。但如果有人重写“Object.prototype.hasOwnProperty”,你会不会遇到同样的问题?无论哪种方式,他们都搞砸了你,这不是你的责任吗? - Scott Rippey
你说 for..in 实践并不差,但可以误用。你有一个真实世界的良好实践的例子,你真的想要通过所有的对象属性,包括继承属性? - rjmunro
@ScottRippey:如果你想把它带到那里: youtube.com/watch?v=FrFUI591WhI - Nathan Wall
有了这个答案,我发现可以访问该值 for (var p in array) { array[p]; } - Equiman


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

  • for..in 将循环遍历数组对象的所有自己和继承的属性 DontEnum;这意味着如果有人将属性添加到特定的数组对象(有正当理由 - 我自己这样做了)或更改了 Array.prototype (这被认为是与其他脚本兼容的代码中的不良实践),这些属性也将被迭代;可以通过检查来排除继承的属性 hasOwnProperty(),但这对于数组对象本身中设置的属性没有帮助

  • for..in 不保证保留元素排序

  • 它很慢,因为你必须遍历数组对象及其整个原型链的所有属性,并且仍然只获取属性的名称,即获取值,将需要额外的查找


101
2018-02-01 14:04





因为...枚举通过保存数组的对象,而不是数组本身。如果我向数组原型链添加一个函数,那么它也将被包含在内。即

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

这将写:

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

因为你永远无法确定什么都不会被添加到原型链中,所以只需使用for循环来枚举数组:

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

这将写:

0 = foo
1 =吧

48
2018-02-01 10:08



数组 是 对象,没有“保存数组的对象”。 - RobG


孤立地,在数组上使用for-in没有任何问题。 For-in迭代对象的属性名称,对于“开箱即用”数组,属性对应于数组索引。 (内置属性如 lengthtoString 等等不包含在迭代中。)

但是,如果您的代码(或您正在使用的框架)将自定义属性添加到数组或数组原型,那么这些属性将包含在迭代中,这可能不是您想要的。

一些JS框架,如Prototype,修改了Array原型。像JQuery这样的其他框架没有,所以使用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


37
2018-02-01 10:34





除了在其他答案中给出的原因之外,如果你需要对计数器变量进行数学运算,你可能不想使用“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);

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


28
2017-09-02 02:29



你可以使用前缀 + 代替 parseInt 除非你真的需要整数或忽略无效字符。 - Konrad Borowski
另外,使用 parseInt() 不推荐。尝试 parseInt("025"); 而且它 将 失败。 - Derek 朕會功夫
@Derek朕会功夫 - 你绝对可以使用 parseInt。问题是如果你不包括基数,旧浏览器可能会尝试解释数字(因此025变为八进制)。这在ECMAScript 5中得到修复,但对于以“0x”开头的数字仍然会发生(它将数字解释为十六进制)。为安全起见,请使用基数来指定数字 parseInt("025", 10)  - 指定基数10。 - IAmTimCorey


截至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。它显示了数组 财产名称


24
2018-03-10 04:29





除了这个事实 for...in 遍历所有可枚举属性(即  与“所有数组元素”相同!),请参阅 http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf,第12.6.4节(第5版)或13.7.5.15(第7版):

机制和 订购 枚举属性... 未指定...

(强调我的。)

这意味着如果浏览器想要,它可以按照插入顺序浏览属性。或者按数字顺序排列。或者按词汇顺序排列(“30”出现在“4”之前!记住所有对象键 - 因此,所有数组索引 - 实际上都是字符串,因此这是完全有意义的)。如果它将对象实现为哈希表,它可以通过桶进行检查。或者采取任何一种方式并添加“向后”。浏览器甚至可以迭代 随机 并且符合ECMA-262标准,只要它访问每个属性一次。

实际上,大多数浏览器目前都喜欢以大致相同的顺序迭代。但没有什么可说的。这是特定的实现,并且如果发现另一种方式更有效,则可以随时改变。

无论哪种方式, for...in 带有它没有秩序的内涵。如果您关心订单,请明确说明并使用常规订单 for 循环索引。


21
2018-05-14 16:26





简短回答:这不值得。


更长的答案:即使不需要顺序元素顺序和最佳性能,它也是不值得的。


答案很长:这不值得,原因如下:

  • 运用 for (var i in array) {} 将导致'array'被解释为任何其他  对象,遍历对象属性链,最终执行速度比基于索引的速度慢 for 循环。
  • 它不能保证按照人们的预期按顺序返回对象属性。
  • 运用 hasOwnProperty() 要么 isNaN() 检查以过滤对象属性是一个额外的开销,导致它执行(甚至更多)更慢。而且,引入这样的附加逻辑否定了首先使用它的关键原因,即由于更简洁的格式。

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


20
2018-03-14 07:12





主要有两个原因:

像其他人所说的那样,你可能会获得不在你的数组中或从原型继承的键。因此,如果,库,则会向Array或Object原型添加属性:

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中实现的方式,而不是作为一个数组。 这似乎是一件小事,但它确实搞砸了应用程序,很难调试。


15
2018-02-04 16:54



Object.keys(a).forEach( function(item) { console.log(item) } )迭代自己的属性键数组,而不是从原型继承的那些。 - Qwerty
是的,但是像for-in循环一样,它不一定是正确的索引顺序。此外,它不适用于不支持ES5的旧版浏览器。 - Lior
你可以教这些浏览器 array.forEach 在脚本中插入某些代码。见Polyfill developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... - Qwerty
当然,但是你正在操纵原型,这并不总是一个好主意......而且,你仍然有订单的问题...... - Lior
当然,原因三:稀疏数组。 - a better oliver