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

有人告诉我不要将for...in用于 JavaScript for...in的数组。为什么不?

答案

原因是一种构造:

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

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
}

正如我之前说过的,要遍历数组或类似数组的对象,最好的方法是使用顺序循环 ,例如普通的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
  }
}

为什么不应该使用for..in遍历数组元素的原因有三个:

  • for..infor..in数组对象的所有自己和继承的属性,这些属性不是DontEnum ;这意味着如果有人将属性添加到特定的数组对象(这样做有充分的理由,我自己做了)或更改了Array.prototype (在代码中被认为是不好的做法,应该与其他脚本一起使用),这些属性也将被迭代;可以通过检查hasOwnProperty()来排除继承的属性,但这不能帮助您在数组对象本身中设置属性

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

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

因为 for ... in 枚举了保存数组的对象,而不是数组本身。如果我将一个函数添加到数组原型链中,则该函数也将包括在内。即

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 = bar
myOwnFunction = function() { alert(this); }

并且由于您永远无法确定什么都不会添加到原型链中,因此只需使用 for 循环来枚举数组:

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

它将写为:

0 = foo
1 = bar

孤立地,在数组上使用 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-loop 将产生 101 个索引,其中 99 的值为undefined

正如 John Slegers 已经注意到的那样,从 2016 年(ES6)起,我们可以将for…of用于数组迭代。

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

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 for...of计数,并且也忽略Array.prototype.foo 。它显示数组

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

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

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

简短的回答:这是不值得的。


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


长答案:这不值得...

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

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

除了一个事实,即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循环。

主要有两个原因:

就像其他人所说的那样,您可能会得到不在数组中或从原型继承的键。所以,如果说,一个库将一个属性添加到 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 中实现的方式,而不是作为数组。这似乎是一件小事,但它确实会使应用程序搞砸,并且很难调试。