Why is using “for…in” with array iteration a bad idea?
我被告知不要将
原因是有一个构造:
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 */ |
另外,规范并不能保证迭代的顺序,这意味着如果您想"迭代"一个数组对象,用这个语句您不能确定属性(数组索引)将按数字顺序访问。
例如,在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 } |
另外,说到继承的属性,例如,如果扩展
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 } |
正如我之前所说,在数组或类似数组的对象上进行迭代,最好是使用一个顺序循环,例如一个普通的老
当您只想枚举对象本身的属性(那些不是继承的属性)时,可以使用
1 2 3 4 5 | for (var prop in obj) { if (obj.hasOwnProperty(prop)) { // prop is not inherited } } |
有些人甚至建议直接从
1 2 3 4 5 | for (var prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { // prop is not inherited } } |
不应使用
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迭代对象的属性名称,并且在"开箱即用"数组的情况下,属性对应于数组索引。(像
但是,如果您的代码(或您使用的框架)向数组或数组原型添加自定义属性,那么这些属性将包含在迭代中,这可能不是您想要的。
一些JS框架,比如原型修改了数组原型。其他框架(如jquery)没有,所以使用jquery可以在中安全地使用。
如果你有疑问,你可能不应该用in。
迭代数组的另一种方法是使用for循环:
1 | for (var ix=0;ix<arr.length;ix++) alert(ix); |
然而,这有一个不同的问题。问题是一个javascript数组可能有"漏洞"。如果您将
1 2 | var arr = ["hello"]; arr[100] ="goodbye"; |
然后数组有两个项,但长度为101。使用for-in将生成两个索引,而for循环将生成101个索引,其中99的值为
除了其他答案中给出的原因之外,如果需要对计数器变量进行数学运算,则可能不希望使用"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),我们可以使用
我只想添加这个简单的演示代码,使事情更清楚:
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 。它显示数组属性名。
除了
The mechanics and order of enumerating the properties ... is not specified...
(强调我的)
这意味着,如果浏览器愿意,它可以按照插入的顺序浏览属性。或者按数字顺序。或者按词汇顺序(其中"30"在"4"之前)!请记住,所有对象键——因此,所有数组索引——实际上都是字符串,所以总的来说是有意义的)。如果它将对象实现为哈希表,那么它可以通过bucket对它们进行遍历。或者把它们中的任何一个加上"向后"。一个浏览器甚至可以随机迭代,并符合ECMA-262,只要它访问了每个属性一次。
实际上,目前大多数浏览器都喜欢以大致相同的顺序进行迭代。但是没有什么可以说他们必须这么做。这是特定于实现的,如果发现另一种方法效率更高,则可以随时更改。
不管怎样,
简短的回答:这不值得。
更长的答案:即使不需要序列元素顺序和最佳性能,它也不值得。
长话短说:这不值得,原因如下:
- 使用
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"得到值,我怀疑你想要这个。
任何对象都可以具有与其关联的任意属性。尤其是在数组实例上加载额外的属性没有什么可怕的。因此,只希望看到类似索引数组的属性的代码必须坚持使用整数索引。完全了解
我认为我没有什么可补充的,例如Triptych的答案或CMS的答案,为什么在某些情况下应该避免使用
不过,我想补充一点,在现代浏览器中,有一种替代方法可以用于不能使用
1 2 3 | for (var item of items) { console.log(item); } |
注:
不幸的是,Internet Explorer的任何版本都不支持此功能(Edge 12+支持),因此您必须等待更长的时间,直到可以在客户端生产代码中使用它。但是,在服务器端JS代码中使用应该是安全的(如果使用node.js)。
另外,由于语义的原因,
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' |
一个重要的方面是,
可以通过以下方式检查属性的可枚举属性值:
1 | myobject.propertyIsEnumerable('myproperty') |
或获取所有四个属性:
1 | Object.getOwnPropertyDescriptor(myobject,'myproperty') |
这是ECMAScript 5中提供的一个功能-在早期版本中,不可能更改可枚举属性的值(它始终设置为true)。
tl&dr:在数组中使用
我认为,如果在数组中正确地使用,
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]); |
所以你看…由于您关心代码,所以
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:"); |
javascript将自动确定其通过项目的方式。因此,如果您知道您的数组确实是非关联的,那么您可以使用
但在我看来,最好是使用
这一问题的真正答案将取决于浏览器解析器/解释JavaScript代码的方式。它可以在浏览器之间更改。
我想不出其他不使用
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]); |
因为如果不小心,它将迭代原型链上属于对象的属性。
您可以使用
您应该只在属性列表上使用
这并不一定是坏的(基于你所做的),但是在数组的情况下,如果在
1 2 | var arr = ['a','b','c']; for (var key in arr) { ... } |
如果在
对数组使用
1.)已经有一个更高阶的函数或方法,用于数组,但具有更多的功能和更精简的语法,称为"foreach":
2.)数组总是有一个长度,但是
3.)标准for循环将执行一个函数,次数与您在参数中定义的次数相同,而且由于数组是编号的,因此定义要执行函数的次数更为合理。与其他循环不同,for循环随后可以为数组中的每个索引执行一个函数,无论该值是否已定义。
本质上,您可以使用任何循环,但您应该准确地记住它们是如何工作的。了解不同循环重复的条件及其各自的功能,并认识到它们或多或少适合于不同的场景。
此外,一般来说,使用
请参见下面的内容,前两个循环只执行一次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 |
你看到其他人了!!!!…在本例中,我们实际上是循环遍历新的数组对象!
所以这就是我们需要小心使用的原因之一,但情况并非总是如此……
尽管这个问题没有具体解决,但我想补充一点,有一个非常好的理由不使用…与
对于单个结果,我得到:
1 2 3 4 5 6 7 8 9 10 11 | var nodes = document.querySelectorAll(selector); nodes ? NodeList [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 |
这就解释了我的
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