关于javascript:为什么在数组迭代中使用”for…in”是一个坏主意?

Why is using “for…in” with array iteration a bad idea?

我被告知不要将for...in与javascript中的数组一起使用。为什么不呢?


原因是有一个构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
*/

有时可能与另一种完全不同:

1
2
3
4
5
6
7
8
9
10
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库可能会执行这样的操作,这将影响您创建的任何数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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)中,即使在数组对象上,枚举的顺序也定义为创建了属性:

1
2
3
4
5
6
7
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对象(像mooolts那样的一些库),那么还将枚举这些属性:

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

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

正如我之前所说,在数组或类似数组的对象上进行迭代,最好是使用一个顺序循环,例如一个普通的老for/while循环。

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

1
2
3
4
5
for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    // prop is not inherited
  }
}

有些人甚至建议直接从Object.prototype调用该方法,以避免在对象中添加名为hasOwnProperty的属性时出现问题:

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


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

  • for..in将循环遍历数组对象的所有自己的和继承的属性,这些属性不是DontEnum;这意味着如果有人向特定数组对象添加属性(这是有正当理由的—我自己已经这样做了)或更改Array.prototype(这在代码中被认为是不好的做法,应该与其他脚本一起工作得很好)s),这些属性也将被迭代;通过检查hasOwnProperty()可以排除继承的属性,但这对数组对象本身设置的属性没有帮助。

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

  • 这很慢,因为您必须遍历数组对象及其整个原型链的所有属性,并且仍然只能获取属性的名称,也就是说,要获取值,需要进行额外的查找。


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

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

这将写下:

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

而且,由于您永远无法确保不会向原型链中添加任何内容,因此只需使用for循环来枚举数组:

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

这将写下:

1
2
0 = foo
1 = bar


在孤立的情况下,在数组中使用for没有任何错误。因为In迭代对象的属性名称,并且在"开箱即用"数组的情况下,属性对应于数组索引。(像lengthtoString等内置属性不包含在迭代中。)

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

一些JS框架,比如原型修改了数组原型。其他框架(如jquery)没有,所以使用jquery可以在中安全地使用。

如果你有疑问,你可能不应该用in。

迭代数组的另一种方法是使用for循环:

1
for (var ix=0;ix<arr.length;ix++) alert(ix);

然而,这有一个不同的问题。问题是一个javascript数组可能有"漏洞"。如果您将arr定义为:

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

然后数组有两个项,但长度为101。使用for-in将生成两个索引,而for循环将生成101个索引,其中99的值为undefined


除了其他答案中给出的原因之外,如果需要对计数器变量进行数学运算,则可能不希望使用"for…in"结构,因为循环迭代对象属性的名称,因此变量是字符串。

例如,

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

将写

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

然而,

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

将写

1
2
3
0, string, 01
1, string, 11
...

当然,这可以通过包括

1
ii = parseInt(ii);

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


截至2016年(ES6),我们可以使用for…of进行数组迭代,正如John Slegers已经注意到的那样。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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++;
    }

控制台显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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。它显示数组属性名。


除了forin循环所有可枚举属性(与"所有数组元素"不同)之外!,见http://www.ecma-international.org/publications/files/ecma-st/ecma-262.pdf,第12.6.4节(第5版)或第13.7.5.15节(第7版):

The mechanics and order of enumerating the properties ... is not specified...

(强调我的)

这意味着,如果浏览器愿意,它可以按照插入的顺序浏览属性。或者按数字顺序。或者按词汇顺序(其中"30"在"4"之前)!请记住,所有对象键——因此,所有数组索引——实际上都是字符串,所以总的来说是有意义的)。如果它将对象实现为哈希表,那么它可以通过bucket对它们进行遍历。或者把它们中的任何一个加上"向后"。一个浏览器甚至可以随机迭代,并符合ECMA-262,只要它访问了每个属性一次。

实际上,目前大多数浏览器都喜欢以大致相同的顺序进行迭代。但是没有什么可以说他们必须这么做。这是特定于实现的,如果发现另一种方法效率更高,则可以随时更改。

不管怎样,forin都没有秩序的内涵。如果您关心订单,就要明确它,并使用带有索引的常规for循环。


简短的回答:这不值得。

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

长话短说:这不值得,原因如下:

  • 使用for (var i in array)将导致array被解释为任何其他纯对象,遍历对象属性链,最终执行速度比基于索引的for循环慢。
  • 不能保证按预期的顺序返回对象属性。
  • 使用hasOwnProperty()isNaN()检查来筛选对象属性是一个额外的开销,导致它执行(甚至更慢)的速度。此外,引入这样的附加逻辑会首先否定使用它的关键原因,也就是说,因为格式更简洁。

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


主要有两个原因:

正如其他人所说,您可能会得到不在数组中或从原型继承的键。因此,假设库向数组或对象原型添加了一个属性:

1
Array.prototype.someProperty = true

您将得到它作为每个数组的一部分:

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

您可以使用hasownProperty方法解决此问题:

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


因为它是通过对象字段而不是索引枚举的。你可以用索引"length"得到值,我怀疑你想要这个。


for ... in ...的问题是,只有当程序员不真正理解语言时,这才成为问题;它不是真正的bug或任何东西,而是它迭代对象的所有成员(好吧,所有可枚举成员,但现在这是一个细节)。当您只想迭代数组的索引属性时,保证语义一致性的唯一方法是使用一个整数索引(即for (var i = 0; i < array.length; ++i)样式的循环)。

任何对象都可以具有与其关联的任意属性。尤其是在数组实例上加载额外的属性没有什么可怕的。因此,只希望看到类似索引数组的属性的代码必须坚持使用整数索引。完全了解for ... in所做工作并真正需要查看所有属性的代码,那么也可以。


我认为我没有什么可补充的,例如Triptych的答案或CMS的答案,为什么在某些情况下应该避免使用for-in

不过,我想补充一点,在现代浏览器中,有一种替代方法可以用于不能使用for-in的情况。另一种选择是for-of

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

注:

不幸的是,Internet Explorer的任何版本都不支持此功能(Edge 12+支持),因此您必须等待更长的时间,直到可以在客户端生产代码中使用它。但是,在服务器端JS代码中使用应该是安全的(如果使用node.js)。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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"


除了其他问题,"for..in"语法可能较慢,因为索引是字符串,而不是整数。

1
2
3
4
5
var a = ["a"]
for (var i in a)
    alert(typeof i)  // 'string'
for (var i = 0; i < a.length; i++)
    alert(typeof i)  // 'number'


一个重要的方面是,for...in只迭代对象中包含的属性,该对象的可枚举属性设置为true。因此,如果试图使用for...in迭代对象,那么如果任意属性的可枚举属性为false,则可能会错过这些属性。很有可能更改普通数组对象的可枚举属性属性,以便不枚举某些元素。尽管一般来说,属性属性倾向于应用于对象内的函数属性。

可以通过以下方式检查属性的可枚举属性值:

1
myobject.propertyIsEnumerable('myproperty')

或获取所有四个属性:

1
Object.getOwnPropertyDescriptor(myobject,'myproperty')

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


tl&dr:在数组中使用for in循环并不是坏事,事实上恰恰相反。

我认为,如果在数组中正确地使用,for in循环是JS的精华。你应该完全控制你的软件,知道你在做什么。让我们看看上面提到的缺点,逐一加以反驳。

  • 它还循环遍历继承的属性:首先,对Array.prototype的所有扩展都应该使用Object.defineProperty()来完成,并且它们的enumerable描述符应该设置为false。任何不这样做的库都不应该被使用。
  • 之后添加到继承链中的属性将被计算在内:当按Object.setPrototypeOf或按extend类进行数组子分类时。您应该再次使用Object.defineProperty(),默认情况下,它将writableenumerableconfigurable属性描述符设置为false。让我们在这里看到一个数组子类的例子…
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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循环现在是安全的。

  • for in循环很慢:当然不是。如果你在稀疏数组上循环,这是迄今为止最快的迭代方法,而稀疏数组需要时间。这是人们应该知道的最重要的性能技巧之一。让我们看一个例子。我们将循环一个稀疏数组。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    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:");


    for/in使用两种类型的变量:哈希表(关联数组)和数组(非关联数组)。

    javascript将自动确定其通过项目的方式。因此,如果您知道您的数组确实是非关联的,那么您可以使用for (var i=0; i<=arrayLen; i++),并跳过自动检测迭代。

    但在我看来,最好是使用for/in,自动检测所需的过程非常小。

    这一问题的真正答案将取决于浏览器解析器/解释JavaScript代码的方式。它可以在浏览器之间更改。

    我想不出其他不使用for/in的目的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //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]);


    因为如果不小心,它将迭代原型链上属于对象的属性。

    您可以使用for.. in,只要确保使用hasownproperty检查每个属性即可。


    您应该只在属性列表上使用for(var x in y),而不在对象上使用(如上所述)。


    这并不一定是坏的(基于你所做的),但是在数组的情况下,如果在Array.prototype中添加了一些东西,那么你会得到奇怪的结果。如果您希望此循环运行三次:

    1
    2
    var arr = ['a','b','c'];
    for (var key in arr) { ... }

    如果在Arrayprototype中添加了一个名为helpfulUtilityMethod的函数,那么您的循环将运行四次:key将是012helpfulUtilityMethod。如果你只期望整数,哎呀。


    对数组使用for...in循环并不是错误的,尽管我可以猜到为什么有人告诉您:

    1.)已经有一个更高阶的函数或方法,用于数组,但具有更多的功能和更精简的语法,称为"foreach":Array.prototype.forEach(function(element, index, array) {} );

    2.)数组总是有一个长度,但是for...inforEach不为'undefined'的任何值执行函数,只为定义了值的索引执行函数。因此,如果只分配一个值,那么这些循环将只执行一次函数,但是由于枚举了一个数组,它的长度将始终达到具有定义值的最高索引,但是使用这些循环时,可能会忽略该长度。

    3.)标准for循环将执行一个函数,次数与您在参数中定义的次数相同,而且由于数组是编号的,因此定义要执行函数的次数更为合理。与其他循环不同,for循环随后可以为数组中的每个索引执行一个函数,无论该值是否已定义。

    本质上,您可以使用任何循环,但您应该准确地记住它们是如何工作的。了解不同循环重复的条件及其各自的功能,并认识到它们或多或少适合于不同的场景。

    此外,一般来说,使用forEach方法比使用for...in循环更好,因为它更容易编写,而且功能更强大,因此您可能希望习惯于只使用此方法和标准,而不使用您的调用。

    请参见下面的内容,前两个循环只执行一次console.log语句,而标准for循环执行指定的函数多次,在本例中,array.length=6。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    var arr = [];
    arr[5] = 'F';

    for (var index in arr) {
    console.log(index);
    console.log(arr[index]);
    console.log(arr)
    }
    // 5
    // 'F'
    // => (6) [undefined x 5, 6]

    arr.forEach(function(element, index, arr) {
    console.log(index);
    console.log(element);
    console.log(arr);
    });
    // 5
    // 'F'
    // => Array (6) [undefined x 5, 6]

    for (var index = 0; index < arr.length; index++) {
    console.log(index);
    console.log(arr[index]);
    console.log(arr);
    };
    // 0
    // undefined
    // => Array (6) [undefined x 5, 6]

    // 1
    // undefined
    // => Array (6) [undefined x 5, 6]

    // 2
    // undefined
    // => Array (6) [undefined x 5, 6]

    // 3
    // undefined
    // => Array (6) [undefined x 5, 6]

    // 4
    // undefined
    // => Array (6) [undefined x 5, 6]

    // 5
    // 'F'
    // => Array (6) [undefined x 5, 6]

    for…in循环始终枚举键。对象属性键始终是字符串,甚至是数组的索引属性:

    1
    2
    3
    4
    5
    6
    var myArray = ['a', 'b', 'c', 'd'];
    var total = 0
    for (elem in myArray) {
      total += elem
    }
    console.log(total); // 00123


    在javascript中处理对象时,for…in很有用,但对于数组则不是,但我们仍然不能说这是错误的方式,但不建议这样做,请看下面的示例,使用for…in循环:

    1
    2
    3
    4
    5
    6
    let txt ="";
    const person = {fname:"Alireza", lname:"Dezfoolian", age:35};
    for (const x in person) {
        txt += person[x] +"";
    }
    console.log(txt); //Alireza Dezfoolian 35

    好的,现在让我们用数组来做:

    1
    2
    3
    4
    5
    6
    let txt ="";
    const person = ["Alireza","Dezfoolian", 35];
    for (const x in person) {
       txt += person[x] +"";
    }
    console.log(txt); //Alireza Dezfoolian 35

    正如你看到的结果一样…

    但是让我们尝试一下,让我们来制作一些要排列的原型…

    1
    Array.prototype.someoneelse ="someoneelse";

    现在我们创建一个新的array();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let txt ="";
    const arr = new Array();
    arr[0] = 'Alireza';
    arr[1] = 'Dezfoolian';
    arr[2] = 35;
    for(x in arr) {
     txt += arr[x] +"";
    }
    console.log(txt); //Alireza Dezfoolian 35 someoneelse

    你看到其他人了!!!!…在本例中,我们实际上是循环遍历新的数组对象!

    所以这就是我们需要小心使用的原因之一,但情况并非总是如此……


    尽管这个问题没有具体解决,但我想补充一点,有一个非常好的理由不使用…与NodeList一起使用(就像从querySelectorAll调用中获得的那样),因为它根本看不到返回的元素,而只在nodelist属性上迭代。

    对于单个结果,我得到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var nodes = document.querySelectorAll(selector);
    nodes
    ? NodeList&nbsp;[a._19eb]
    for (node in nodes) {console.log(node)};
    VM505:1 0
    VM505:1 length
    VM505:1 item
    VM505:1 entries
    VM505:1 forEach
    VM505:1 keys
    VM505:1 values

    这就解释了我的for (node in nodes) node.href = newLink;失败的原因。


    Since JavaScript elements are saved as standard object properties, it
    is not advisable to iterate through JavaScript arrays using for...in
    loops because normal elements and all enumerable properties will be
    listed.

    来自https://developer.mozilla.org/en-us/docs/web/javascript/guide/indexed_collections