为什么使用“for...in”进行数组迭代是一个坏主意?
- 2024-11-02 21:00:00
- admin 原创
- 35
问题描述:
有人告诉我不要for...in
在 JavaScript 中使用数组。为什么不呢?
解决方案 1:
原因在于一个构造:
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
*/
运行代码片段Hide results展开片段
有时可能会完全不同于另一个:
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
*/
运行代码片段Hide results展开片段
还请考虑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
*/
运行代码片段Hide results展开片段
解决方案 2:
该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
}
}
解决方案 3:
有三个原因导致您不应该使用for..in
迭代数组元素:
for..in
将循环遍历数组对象的所有自身属性和继承属性,这些属性不是DontEnum
;这意味着如果有人向特定数组对象添加属性(这样做有正当理由 - 我自己就这样做过)或更改属性Array.prototype
(这在应该与其他脚本配合良好的代码中被认为是不好的做法),这些属性也将被迭代;可以通过检查排除继承的属性hasOwnProperty()
,但这对数组对象本身中设置的属性没有帮助for..in
不能保证保留元素顺序它很慢,因为你必须遍历数组对象的所有属性及其整个原型链,并且仍然只能获取属性的名称,即要获取值,需要进行额外的查找
解决方案 4:
因为 for...in 枚举的是保存数组的对象,而不是数组本身。如果我将一个函数添加到数组原型链中,那么它也将被包括在内。即
Array.prototype.myOwnFunction = function() { alert(this); }
a = new Array();
a[0] = 'foo';
a[1] = 'bar';
for (var x in a) {
document.write(x + ' = ' + a[x]);
}
这将写入:
0 = foo
1 = bar
myOwnFunction = function() { alert(this); }
因为你永远无法确定原型链中是否会添加任何内容,因此只需使用 for 循环来枚举数组:
for (var i=0,x=a.length; i<x; i++) {
document.write(i + ' = ' + a[i]);
}
这将写入:
0 = foo
1 = bar
解决方案 5:
从 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: 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
。它显示数组属性名称。
解决方案 6:
简短的回答是:这不值得。
较长的答案:即使不需要顺序元素顺序和最佳性能,这也是不值得的。
长答案:这不值得……
使用
for (var property in array)
将导致作为对象array
进行迭代,遍历对象原型链,最终执行速度比基于索引的循环慢。for
for (... in ...)
不能保证像人们期望的那样按顺序返回对象属性。使用
hasOwnProperty()
和!isNaN()
检查来过滤对象属性是一个额外的开销,导致其执行速度更慢,并且否定了首先使用它的关键原因,即因为格式更简洁。
由于这些原因,性能和便利性之间根本就不存在可接受的权衡。除非意图将数组作为对象处理并对数组的对象属性执行操作,否则实际上没有任何好处。
解决方案 7:
单独来看,在数组上使用 for-in 并没有什么问题。For-in 会遍历对象的属性名称,对于“现成的”数组,属性对应于数组索引。(内置属性,如length
等toString
,不包含在迭代中。)
但是,如果您的代码(或您正在使用的框架)向数组或数组原型添加自定义属性,那么这些属性将包含在迭代中,这可能不是您想要的。
某些 JS 框架(例如 Prototype)会修改数组原型。其他框架(例如 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 循环将产生 101 个索引,其中 99 的值为undefined
。
解决方案 8:
除了其他答案中给出的原因之外,如果您需要使用计数器变量进行数学运算,您可能不想使用“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);
在循环中,但第一个结构更直接。
解决方案 9:
除了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
带索引的常规循环。
解决方案 10:
主要有两点原因:
一
就像其他人所说的那样,您可能会获得数组中没有的键或从原型继承的键。因此,假设一个库向数组或对象原型添加一个属性:
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 中的实现方式),而不是数组。这似乎是一件小事,但它确实会搞砸应用程序,并且很难调试。
解决方案 11:
我认为对于为什么在某些情况下应避免使用,例如Triptych 的回答或CMS 的回答,我没有什么可补充的。for...in
不过,我想补充一点,在现代浏览器中,有一种替代方案for...in
可以在for...in
无法使用的情况下使用。该替代方案是for...of
:
for (var item of items) {
console.log(item);
}
笔记 :
不幸的是,没有一个版本的 Internet Explorer 支持for...of
(Edge 12+支持),所以你得等一段时间才能在客户端生产代码中使用它。但是,在服务器端 JS 代码中使用它应该是安全的(如果你使用Node.js)。
解决方案 12:
因为它通过对象字段而不是索引进行枚举。您可以使用索引“长度”获取值,但我怀疑您是否想要这样做。
解决方案 13:
问题在于for ... in ...
——只有当程序员不真正理解该语言时,这才会成为问题;这实际上不是一个错误或任何东西——它会迭代对象的所有成员(好吧,所有可枚举成员,但这只是目前的细节)。当您只想迭代数组的索引属性时,唯一能保证语义一致的方法是使用整数索引(即for (var i = 0; i < array.length; ++i)
样式循环)。
任何对象都可以具有与其关联的任意属性。特别是,将其他属性加载到数组实例上并没有什么可怕的。因此,想要仅查看索引数组类属性的代码必须坚持使用整数索引。如果代码完全了解for ... in
并真正需要查看所有属性,那么这也是可以的。
解决方案 14:
TL&DR:在数组中使用for in
循环并不是什么坏事,事实上恰恰相反。
我认为如果在数组中正确for in
使用循环,它就是 JS 的瑰宝。你应该完全控制你的软件,并且知道自己在做什么。让我们看看提到的缺点,并逐一反驳它们。
它还循环遍历继承的属性:首先,对 的任何扩展都
Array.prototype
应该使用 来完成Object.defineProperty()
,并且它们的enumerable
描述符应该设置为false
。任何没有这样做的库都不应该被使用。稍后添加到继承链中的属性会被计算在内:当通过
Object.setPrototypeOf
或 通过 Class进行数组子类化时extend
。您应该再次使用Object.defineProperty()
默认情况下将 和 属性描述符设置writable
为。让我们在这里看一个数组子类化示例...enumerable
`configurable`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]);
运行代码片段Hide results展开片段
所以你看..for in
循环现在是安全的,因为你关心你的代码。
循环
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:");
运行代码片段Hide results展开片段
解决方案 15:
此外,由于语义原因,处理数组的方式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"
解决方案 16:
以下是为什么这种做法(通常)是不好的做法的原因:
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);
}
运行代码片段Hide results展开片段
for...in
循环不保证特定的迭代顺序。尽管这种顺序在当今大多数现代浏览器中都很常见,但仍然不能 100% 保证。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('
');
// for in does ignore the undefined elements
for (let el in arr) {
console.log(arr[el]);
}
运行代码片段Hide results展开片段
解决方案 17:
除了其他问题之外,“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'
解决方案 18:
一个重要的方面是,for...in
只迭代对象中包含的可枚举 属性设置为 true 的属性。因此,如果尝试使用迭代对象,for...in
则如果可枚举属性为 false,则可能会错过任意属性。完全可以更改普通数组对象的可枚举属性,以便不枚举某些元素。尽管一般来说,属性属性往往适用于对象内的函数属性。
可以通过以下方式检查属性的可枚举属性的值:
myobject.propertyIsEnumerable('myproperty')
或者获取所有四个属性:
Object.getOwnPropertyDescriptor(myobject,'myproperty')
这是 ECMAScript 5 中的一项功能 - 在早期版本中,无法改变可枚举属性的值(它始终设置为 true)。
解决方案 19:
/适用于两种类型的变量:哈希表(关联数组)和数组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]);
解决方案 20:
因为如果您不小心,它将遍历原型链上属于对象的属性。
您可以使用for.. in
,但一定要使用hasOwnProperty检查每个属性。
解决方案 21:
这不一定是坏事(根据你正在做的事情),但在数组的情况下,如果向 添加了某些内容Array.prototype
,那么你将得到奇怪的结果。你预计此循环将运行三次:
var arr = ['a','b','c'];
for (var key in arr) { ... }
如果在中helpfulUtilityMethod
添加了一个名为 的函数,则循环最终会运行四次:分别为、、和。如果您只期望整数,那就糟糕了。Array
`prototypekey
01
2`helpfulUtilityMethod
解决方案 22:
您应该for(var x in y)
只在属性列表上使用,而不是在对象上使用(如上所述)。
解决方案 23:
for...in
对数组使用循环并没有错,尽管我可以猜测为什么有人会告诉你这一点:
1.) 已经有一个高阶函数或方法,具有用于数组的用途,但具有更多功能和更精简的语法,称为“forEach”:Array.prototype.forEach(function(element, index, array) {} );
2.) 数组始终具有长度,但for...in
和forEach
不会对任何 值执行函数'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]
解决方案 24:
for...in 循环始终枚举键。对象属性的键始终是字符串,即使是数组的索引属性也是如此:
var myArray = ['a', 'b', 'c', 'd'];
var total = 0
for (elem in myArray) {
total += elem
}
console.log(total); // 00123
解决方案 25:
for...in在 JavaScript 中处理对象时很有用,但对于数组则不然,但我们不能说这是一种错误的方法,但不推荐这样做,请看下面使用for...in循环的示例:
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 对象!
所以这就是我们需要谨慎使用for..in 的原因之一,但情况并非总是如此......
解决方案 26:
由于 JavaScript 元素被保存为标准对象属性,因此不建议使用 for...in 循环遍历 JavaScript 数组,因为这样会列出普通元素和所有可枚举属性。
来自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections
解决方案 27:
尽管这个问题没有具体解决,但我要补充一点,有一个很好的理由永远不要使用 for...in NodeList
(就像从调用中获得的那样querySelectorAll
,因为它根本看不到返回的元素,而是只迭代 NodeList 属性。
在只有单一结果的情况下,我得到了:
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
这解释了为什么我的for (node in nodes) node.href = newLink;
失败了。
解决方案 28:
for in 循环在遍历数组时将索引转换为字符串。例如,在下面的代码中,在第二个循环中,用 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]);
运行代码片段Hide results展开片段
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件