为什么在数组迭代中使用“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个回答

17

我认为对于如 Triptych 的回答 CMS 的回答中所述,关于在某些情况下应避免使用for...in, 我没有太多要补充的。

然而,我想要补充的是,在现代浏览器中,有一种可以替代for...in并可在for...in无法使用的情况下使用的方法。这种方法是使用for...of

for (var item of items) {
    console.log(item);
}

注意:

很遗憾,任何版本的Internet Explorer都不支持for...ofEdge 12+可以),因此你需要等待一段时间才能在客户端生产代码中使用它。然而,在服务器端JS代码中使用它应该是安全的(如果您使用Node.js)。


16
因为它枚举对象字段,而不是索引。您可以使用索引“length”获取值,但我怀疑您不想这样做。

3
在 Firefox 3 中,你可以使用 arr.forEach 或 for(var [i, v] in Iterator(arr)) {},但两者都无法在 IE 中运行,不过你可以自己编写 forEach 方法。 - vava
5
答案错误。 "lenght" 不会被包含在 for-in 迭代中。只有您自己添加的属性才会被包括在内。 - JacquesB

15
< p >问题在于for...in...——只有当程序员不真正理解语言时,才会出现这个问题;这不是一个真正的错误或任何其他东西——它遍历对象的所有成员(好吧,所有可枚举成员,但现在不需要细节)。 当您想要迭代数组的索引属性时,保持语义一致的唯一保证方式是使用整数索引(也就是说,像 for(var i = 0; i < array.length; ++i)这样的循环)。

任何对象都可以具有与其关联的任意属性。 尤其是将其他属性加载到数组实例上并没有什么可怕的事情。希望仅查看索引数组属性的代码因此必须坚持整数索引。 完全了解 for ... in 的工作原理和真正需要查看所有属性的代码,那么也可以。


很好的解释,Pointy。只是好奇,如果我有一个包含在多个属性下的对象内部的数组,并使用for in,相比于常规的for循环,这些数组会被迭代吗?(这本质上会降低性能,对吧?) - NiCk Newman
2
@NiCkNewman 嗯,在 for ... in 循环中,你引用 in 后面的对象将只是一个简单的迭代器。 - Pointy
我明白了。只是好奇,因为我的主游戏对象中有对象和数组,想知道对于局内比赛来说,这是否比仅使用索引的常规for循环更痛苦。 - NiCk Newman
@NiCkNewman,整个问题的主题是你不应该在数组上使用for ... in循环;有许多很好的理由不这样做。这不仅仅是性能问题,更是“确保它不会出错”的问题。 - Pointy
我的对象在技术上存储在一个数组中,这就是我担心的原因,类似于:[{a:'嗨',b:'你好'},{a:'嗨',b:'你好'}],但是我理解了。 - NiCk Newman

12
TL&DR: 在数组中使用for in循环不是邪恶的行为,事实上相反。
我认为如果在数组中正确使用,for in循环是JS的一个宝石。您可以完全控制您的软件并知道自己在做什么。让我们看看提到的缺点并逐一反驳它们。
  1. 它也遍历继承属性: 首先,任何对Array.prototype的扩展都应该使用Object.defineProperty()进行,并且它们的enumerable描述符应设置为false。任何没有这样做的库都不应该被使用。
  2. 后期添加到继承链中的属性也会被计数: 当通过Object.setPrototypeOf或Class extend进行数组子类化时。您应该再次使用Object.defineProperty(),默认情况下将writableenumerableconfigurable属性描述符设置为false。让我们在这里看一个数组子类化示例...

function Stack(...a){
  var stack = new Array(...a);
  Object.setPrototypeOf(stack, Stack.prototype);
  return stack;
}
Stack.prototype = Object.create(Array.prototype);                                 // now stack has full access to array methods.
Object.defineProperty(Stack.prototype,"constructor",{value:Stack});               // now Stack is a proper constructor
Object.defineProperty(Stack.prototype,"peak",{value: function(){                  // add Stack "only" methods to the Stack.prototype.
                                                       return this[this.length-1];
                                                     }
                                             });
var s = new Stack(1,2,3,4,1);
console.log(s.peak());
s[s.length] = 7;
console.log("length:",s.length);
s.push(42);
console.log(JSON.stringify(s));
console.log("length:",s.length);

for(var i in s) console.log(s[i]);

所以你看到了…… for in 循环现在是安全的,因为你关心你的代码。

  1. for in 循环很慢:绝对不是。如果你需要间歇地循环稀疏数组,它是迭代速度最快的方法。这是一个非常重要的性能技巧,每个人都应该知道。让我们看一个例子,我们将循环遍历一个稀疏数组。

var a = [];
a[0] = "zero";
a[10000000] = "ten million";
console.time("for loop on array a:");
for(var i=0; i < a.length; i++) a[i] && console.log(a[i]);
console.timeEnd("for loop on array a:");
console.time("for in loop on array a:");
for(var i in a) a[i] && console.log(a[i]);
console.timeEnd("for in loop on array a:");


10
以下是这种做法(通常)不好的原因:
  1. for...in 循环遍历它们自己所有可枚举的属性以及它们的原型的可枚举属性。在数组迭代中,我们通常只想遍历数组本身。即使您自己不向数组添加任何内容,您的库或框架可能会添加一些东西。
示例:

Array.prototype.hithere = 'hithere';

var array = [1, 2, 3];
for (let el in array){
    // the hithere property will also be iterated over
    console.log(el);
}

  1. for...in循环不保证特定的迭代顺序。尽管这个顺序在大多数现代浏览器中是可见的,但仍不能100%地保证。
  2. for...in循环会忽略undefined数组元素,即尚未被分配的数组元素。

示例::

const arr = []; 
arr[3] = 'foo';   // resize the array to 4
arr[4] = undefined; // add another element with value undefined to it

// iterate over the array, a for loop does show the undefined elements
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

console.log('\n');

// for in does ignore the undefined elements
for (let el in arr) {
    console.log(arr[el]);
}


10

另外,由于语义的原因,for in 处理数组的方式(即与任何其他JavaScript对象相同)与其他流行语言不一致。

// C#
char[] a = new char[] {'A', 'B', 'C'};
foreach (char x in a) System.Console.Write(x); //Output: "ABC"

// Java
char[] a = {'A', 'B', 'C'};
for (char x : a) System.out.print(x);          //Output: "ABC"

// PHP
$a = array('A', 'B', 'C');
foreach ($a as $x) echo $x;                    //Output: "ABC"

// JavaScript
var a = ['A', 'B', 'C'];
for (var x in a) document.write(x);            //Output: "012"

这里的论点是,与众不同是一件坏事吗? - Lee Goddard
@LeeGoddard 完全没有,只是在指出通常情况下使用 for...in 循环遍历“值”,而不是“键”。JS 的行为并不是一件坏事,它可能对许多来自其他流行编程语言的开发人员来说很难理解错误,然后他们可能不愿意在这种情况下使用 for...in。这就是答案指出的所有内容,它作为一个缺点添加,当然还有辩论的余地。 - matpop
@matpoop - 保持不同!来自前共产主义东方的问候。 - Lee Goddard

8
for/in语句适用于两种类型的变量:哈希表(关联数组)和数组(非关联)。JavaScript会自动确定其传递的方式。所以如果你知道你的数组确实是非关联的,可以使用for (var i=0; i<=arrayLen; i++),跳过自动检测迭代。
但在我看来,最好使用for/in,因为该自动检测所需的处理非常小。对此的真正答案将取决于浏览器解析/解释JavaScript代码的方式。它可能在浏览器之间有所不同。
我想不到其他不使用for/in语句的目的;
//Non-associative
var arr = ['a', 'b', 'c'];
for (var i in arr)
   alert(arr[i]);

//Associative
var arr = {
   item1 : 'a',
   item2 : 'b',
   item3 : 'c'
};

for (var i in arr)
   alert(arr[i]);

2
for ... in 适用于对象。并不存在自动检测的情况。 - a better oliver

8
重要的是,for...in 只会迭代对象中那些其可枚举属性设置为 true 的属性。因此,如果尝试使用 for...in 迭代一个对象,则在其可枚举属性设置为 false 时,可能会错过任意属性。可以改变普通数组对象的可枚举属性,以便不枚举某些元素,但通常情况下,属性属性适用于对象内的函数属性。
可以通过以下方式检查属性的可枚举属性值:
myobject.propertyIsEnumerable('myproperty')

或者获取所有四个属性:

Object.getOwnPropertyDescriptor(myobject,'myproperty')

这是 ECMAScript 5 中提供的一个功能 - 在早期版本中,无法更改可枚举属性的值(它始终设置为 true)。


8
除了其他问题外,“for..in”语法可能会更慢,因为索引是一个字符串,而不是整数。
var a = ["a"]
for (var i in a)
    alert(typeof i)  // 'string'
for (var i = 0; i < a.length; i++)
    alert(typeof i)  // 'number'

可能并不是很重要。数组元素是基于数组或类似数组的对象的属性,而所有对象属性都有字符串键。除非您的JS引擎以某种方式进行了优化,即使您使用数字,它最终也会被转换为字符串进行查找。 - cHao
无论有任何性能问题,如果你是 JavaScript 的新手,请使用 var i in a 并期望索引是一个整数,那么像 a[i+offset] = <value> 这样的操作将会把值放在完全错误的位置。("1" + 1 == "11")。 - szmoore

7
因为如果不小心的话,它会迭代属于对象原型链上的属性。您可以使用for..in,但一定要使用hasOwnProperty检查每个属性。

2
不够 - 向数组实例添加任意命名属性是完全可以的,这些属性将通过 hasOwnProperty() 检查测试为 true - Pointy
好的,谢谢。我从来没有傻到这样对待一个数组,所以我没有考虑过这个问题! - JAL
1
@Pointy 我没有测试过,但也许可以通过在每个属性名称上使用 isNaN 检查来克服这个问题。 - WynandB
1
@Wynand 有趣的想法;然而,当使用简单的数字索引进行迭代如此容易时,我真的不明白为什么值得费这个劲。 - Pointy
@WynandB 不好意思打扰一下,但我觉得需要纠正一下:isNaN 用于检查变量是否为特殊值 NaN,不能用于检查“非数字的内容”(你可以使用常规 typeof 进行检查)。 - doldt

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