为什么使用“for… in”进行数组迭代是个坏主意?

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

987792 次浏览

因为它通过对象字段枚举,而不是索引。您可以使用索引“长度”获得值,我怀疑您是否想要这个。

原因是一个构造:

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

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

var a = [];a[5] = 5;for (var x in a) {// Shows only the explicitly set index of "5", and ignores 0-4console.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:01234foo*/

因为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 = foo1 = barmyOwnFunction = function() { alert(this); }

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

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

这将写入:

0 = foo1 = bar

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

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

一些JS框架,如原型修改数组原型。其他框架如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

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

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

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

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

除了其他答案中给出的原因之外,如果您需要对计数器变量进行数学运算,您可能不想使用“for… in”结构,因为循环遍历对象属性的名称,因此变量是一个字符串。

例如,

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

会写

0, number, 11, number, 2...

然而,

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

会写

0, string, 011, string, 11...

当然,这可以很容易地克服,包括

ii = parseInt(ii);

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

因为如果你不小心的话,它会遍历原型链上属于对象的属性。

您可以使用for.. in,但请务必使用hasOwnProperty检查每个属性。

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 ...的问题-只有当程序员并不真正理解该语言时才会成为问题;它不是真正的bug或任何东西-是它迭代对象的所有成员(好吧,所有enumerable成员,但这是现在的细节)。当你想迭代只是数组的索引属性时,保持语义一致的唯一保证方法是使用整数索引(即for (var i = 0; i < array.length; ++i)样式循环)。

任何对象都可以有与其关联的任意属性。特别是在数组实例上加载额外的属性没有什么可怕的。代码想要看到只有索引的类似数组的属性,因此必须坚持使用整数索引。代码完全知道for ... in做了什么,实际上需要看到所有属性,那也没关系。

这不是必然坏(基于你正在做的事情),但在数组的情况下,如果有东西被添加到Array.prototype,那么你会得到奇怪的结果。你期望这个循环运行三次的地方:

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

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

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

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

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

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

对此的真正答案将取决于浏览器如何解析/解释JavaScript代码。它可以在浏览器之间更改。

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

//Non-associativevar arr = ['a', 'b', 'c'];for (var i in arr)alert(arr[i]);
//Associativevar arr = {item1 : 'a',item2 : 'b',item3 : 'c'};
for (var i in arr)alert(arr[i]);

除了forin循环遍历所有可枚举属性(没有与“所有数组元素”相同!),请参阅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,只要它只访问每个属性一次。

在实践中,大多数浏览器目前都喜欢以大致相同的顺序迭代。但没有什么说他们必须这样做。这是特定于实现的,如果发现另一种方式更有效,随时可能改变。

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

除了其他问题之外,“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'

简短的回答:这不值得。


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


长长的回答:只是不值得…

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

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

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

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

myobject.propertyIsEnumerable('myproperty')

或者获取所有四个属性属性:

Object.getOwnPropertyDescriptor(myobject,'myproperty')

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

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

// C#char[] a = new char[] {'A', 'B', 'C'};foreach (char x in a) System.Console.Write(x); //Output: "ABC"
// Javachar[] 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"
// JavaScriptvar a = ['A', 'B', 'C'];for (var x in a) document.write(x);            //Output: "012"

主要有两个原因:

一个

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

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

我不认为我有很多要补充的,例如三联的回答CMS的回答,为什么在某些情况下应该避免使用for...in

但是,我确实想补充一下在现代浏览器,在不能使用for...in的情况下,可以使用for...in的替代方案。那个替代方案是#2

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

备注:

不幸的是,没有Internet Explorer版本支持for...of边缘12+支持),因此您必须等待一段时间才能在客户端生产代码中使用它。但是,在您的服务器端JS代码中使用它应该是安全的(如果您使用Node.js)。

从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: undefined1: undefined2: undefined3: undefined4: undefined5: xyz
for...in:
0: 51: foo

换句话说:

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

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

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

我认为如果在数组中使用正确for in循环是JS的瑰宝。您应该完全控制您的软件并知道您在做什么。让我们看看提到的缺点并逐一反驳它们。

  1. 它还循环遍历继承的属性:首先,对Array.prototype的任何扩展都应该使用#1完成,并且它们的enumerable描述符应该设置为false。任何不这样做的库都不应该被使用。
  2. 稍后添加到继承链中的属性将被计数:当通过Object.setPrototypeOf或Classextend进行数组子类化时。您应该再次使用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 constructorObject.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. #0循环很慢:当然不是。如果你在不时需要的稀疏数组上循环,这是迄今为止最快的迭代方法。这是一个人应该知道的最重要的性能技巧之一。让我们看一个例子。我们将循环一个稀疏数组。

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循环并没有错,尽管我可以猜到为什么有人告诉你:

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

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

3.)标准的for循环将执行一个函数的次数与你在参数中定义的次数一样多,而且由于数组是编号的,定义你想执行一个函数的次数更有意义。与其他循环不同,for循环可以为数组中的每个索引执行一个函数,无论值是否定义。

从本质上讲,你可以使用任何循环,但你应该确切地记住它们是如何工作的。了解不同循环重复的条件,它们的独立功能,并意识到它们或多或少适合不同的场景。

此外,一般来说,使用forEach方法可能比使用for...in循环更好,因为它更容易编写并且具有更多功能,因此您可能希望养成仅使用此方法和标准的习惯。

请参阅下面,前两个循环只执行一次console.log语句,而标准的for循环执行指定次数的函数,在这种情况下,array.length=6。

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]

为…在在处理JavaScript中的对象时很有用,但不适用于数组,但我们仍然不能说这是错误的方法,但不推荐,看看下面使用为…在循环的例子:

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

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

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

正如你看到的结果一样…

但是让我们尝试一些东西,让我们原型数组

Array.prototype.someoneelse = "someoneelse";

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

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

您会看到别人!!!…在这种情况下,我们实际上循环遍历了新的Array对象!

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

由于JavaScript元素保存为标准对象属性,因此不建议使用for… in遍历JavaScript数组循环,因为普通元素和所有可枚举属性将是列表。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections

for… in循环总是枚举键。对象属性键始终是String,即使是数组的索引属性:

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

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

在单一结果的情况下,我得到了:

var nodes = document.querySelectorAll(selector);nodes▶ NodeList [a._19eb]for (node in nodes) {console.log(node)};VM505:1 0VM505:1 lengthVM505:1 itemVM505:1 entriesVM505:1 forEachVM505:1 keysVM505:1 values

这解释了为什么我的for (node in nodes) node.href = newLink;失败了。

以下是为什么这(通常)是一个糟糕的做法的原因:

  1. for...in循环迭代它们自己的所有可枚举属性它们原型的可枚举属性。通常在数组迭代中,我们只想迭代数组本身。即使你自己可能不会向数组添加任何东西,你的库或框架也可能会添加一些东西。

示例

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

  1. for...in循环不保证特定的迭代顺序。虽然现在大多数现代浏览器中通常看到的是顺序,但仍然没有100%的保证。
  2. for...in循环忽略undefined数组元素,即尚未分配的数组元素。

示例:

const arr = [];arr[3] = 'foo';   // resize the array to 4arr[4] = undefined; // add another element with value undefined to it
// iterate over the array, a for loop does show the undefined elementsfor (let i = 0; i < arr.length; i++) {console.log(arr[i]);}
console.log('\n');
// for in does ignore the undefined elementsfor (let el in arr) {console.log(arr[el]);}

for in loop在遍历数组时将索引转换为字符串。例如,在下面的代码中,在第二个循环中,用i+1初始化j,i是索引,但在字符串中(“0”、“1”等),js中的数字+字符串是字符串。如果js遇到“0”+1,它将返回“01”。

var maxProfit = function(prices) {let maxProfit = 0;for (let i in prices) {for (let j = i + 1; j < prices.length; j++) {console.log(prices[j] - prices[i], "i,j", i, j, typeof i, typeof j);if ((prices[j] - prices[i]) > maxProfit) maxProfit = (prices[j] - prices[i]);}}return maxProfit;};
maxProfit([7, 1, 5, 3, 6, 4]);