如何使用 JavaScript 遍历数组中的所有条目?
我以为是这样的:
forEach(instance in theArray)
theArray
我的数组在哪里,但这似乎不正确。
如何使用 JavaScript 遍历数组中的所有条目?
我以为是这样的:
forEach(instance in theArray)
theArray
我的数组在哪里,但这似乎不正确。
TL; 博士
你最好的选择通常是
for-of
循环(仅限 ES2015+;规范| MDN)——简单且async
友好
for (const element of theArray) {
// ...use `element`...
}
forEach
(ES5 +只;规格| MDN)(或其亲属some
和这样) -不 async
-友好(而见详情)
theArray.forEach(element => {
// ...use `element`...
});
for
循环——async
友好
for (let index = 0; index < theArray.length; ++index) {
const element = theArray[index];
// ...use `element`...
}
for-in
有保护措施- -async
友好
for (const propertyName in theArray) {
if (/*...is an array element property (see below)...*/) {
const element = theArray[propertyName];
// ...use `element`...
}
}
一些快速的“不要”:
但是还有很多东西要探索,请继续阅读...
JavaScript 具有强大的语义来循环遍历数组和类数组对象。我将答案分为两部分:真正数组的选项,以及类似数组的选项,例如arguments
对象、其他可迭代对象 (ES2015+)、DOM 集合等。
好的,让我们看看我们的选择:
您有五个选项(两个基本上永久支持,另一个由 ECMAScript 5 [“ES5”] 添加,另外两个在 ECMAScript 2015(“ES2015”,又名“ES6”)中添加:
for-of
(隐式使用迭代器)(ES2015+)forEach
及相关(ES5+)for
循环for-in
(你可以在这里看到那些旧的规范:ES5,ES2015,但两者都被取代了;当前编辑的草稿总是在这里。)
细节:
for-of
(隐式使用迭代器)(ES2015+)ES2015向 JavaScript添加了迭代器和可迭代对象。数组是可迭代的(字符串、Map
s 和Set
s 以及 DOM 集合和列表也是如此,稍后您将看到)。可迭代对象为其值提供迭代器。newfor-of
语句循环遍历迭代器返回的值:
const a = ["a", "b", "c"];
for (const element of a) { // You can use `let` instead of `const` if you like
console.log(element);
}
// a
// b
// c
没有比这更简单的了!在幕后,它从数组中获取一个迭代器并遍历迭代器返回的值。数组提供的迭代器提供数组元素的值,从开始到结束。
注意element
每个循环迭代的范围如何;尝试element
在循环结束后使用会失败,因为它不存在于循环体之外。
理论上,一个for-of
循环涉及多个函数调用(一个用于获取迭代器,然后一个用于从中获取每个值)。即使这是真的,也没有什么可担心的,现代 JavaScript 引擎中的函数调用非常便宜(它困扰着我forEach
[below] 直到我研究它;细节)。但此外,在处理数组等本机迭代器时,JavaScript 引擎会优化这些调用(在性能关键代码中)。
for-of
是完全async
友好的。如果您需要在循环体中串联(而不是并行)完成工作,循环体中的一个await
in 循环体将在继续之前等待Promise解决。这是一个愚蠢的例子:
请注意这些词是如何在每个词之前延迟出现的。
这是编码风格的问题,但这for-of
是我在遍历任何可迭代的东西时首先要接触的东西。
forEach
及相关在任何可以访问Array
ES5 添加的功能的模糊现代环境(因此,不是 IE8)中,如果您只处理同步代码(或者您不需要等待),则可以使用forEach
( spec | MDN )用于在循环期间完成的异步进程):
const a = ["a", "b", "c"];
a.forEach((element) => {
console.log(element);
});
forEach
接受一个回调函数和一个可选的值,作为this
调用该回调时使用的值(上面未使用)。为数组中的每个元素调用回调,按顺序跳过稀疏数组中不存在的元素。虽然我只使用了上面的一个参数,但回调函数是用三个参数调用的:该迭代的元素、该元素的索引以及对您正在迭代的数组的引用(以防您的函数还没有它便利)。
像for-of
,forEach
的优点是您不必在包含范围内声明索引和值变量;在这种情况下,它们作为迭代函数的参数提供,并且很好地限定在该迭代中。
与for-of
,forEach
的缺点是它不理解async
函数和await
. 如果您使用async
函数作为回调,forEach
则在继续之前不等待该函数的Promise解决。这是使用替代的async
示例- 请注意初始延迟是如何出现的,但随后所有文本都会立即出现而不是等待:for-of
forEach
forEach
是“循环遍历所有”函数,但 ES5 定义了其他几个有用的“遍历数组并执行操作”函数,包括:
every
( spec | MDN ) - 回调第一次返回假值时停止循环some
( spec | MDN ) - 回调第一次返回真值时停止循环filter
( spec | MDN ) - 创建一个新数组,其中包含回调返回真值的元素,省略不返回真值的元素map
( spec | MDN ) - 从回调返回的值创建一个新数组reduce
( spec | MDN ) - 通过重复调用回调来建立一个值,传入以前的值;有关详细信息,请参阅规范reduceRight
( spec | MDN ) - 类似reduce
,但按降序而不是升序工作和 一样forEach
,如果你使用一个async
函数作为你的回调函数,那么这些函数都不会等待函数的Promise完成。这意味着:
async
回调函数从来没有通过适当的every
,some
以及filter
因为它们将把返回的Promise,就好像是一个truthy值; 他们不会等待Promise解决,然后使用履行value。async
函数回调通常适用于map
,如果目标是将某物的数组转换为promise的数组,可能是为了传递给其中一个 promise 组合器函数(Promise.all
、Promise.race
、promise.allSettled
、 或Promise.any
)。async
函数回调很少适合与reduce
or reduceRight
,因为(再次)回调将始终返回一个Promise。但是有一种习惯用法是从使用reduce
( const promise = array.reduce((p, element) => p.then(/*...something using `element`...*/));
)的数组构建Promise链,但通常在这些情况下,函数中的for-of
orfor
循环async
会更清晰且更易于调试。for
循环有时旧的方法是最好的:
const a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
const element = a[index];
console.log(element);
}
如果数组的长度将不会在循环过程中改变,它在高的性能敏感的代码,一个稍微复杂一点的版本抓住了长度达阵可能是一个很小的有点快:
const a = ["a", "b", "c"];
for (let index = 0, len = a.length; index < len; ++index) {
const element = a[index];
console.log(element);
}
和/或倒数:
const a = ["a", "b", "c"];
for (let index = a.length - 1; index >= 0; --index) {
const element = a[index];
console.log(element);
}
但是对于现代 JavaScript 引擎,您很少需要勉强挤出最后一点力气。
在 ES2015 之前,循环变量必须存在于包含作用域中,因为var
只有函数级作用域,没有块级作用域。但是正如您在上面的示例中看到的那样,您可以let
在 内使用for
将变量范围限定为循环。当你这样做时,index
每次循环迭代都会重新创建变量,这意味着在循环体中创建的闭包会保留index
对特定迭代的引用,这解决了旧的“循环中的闭包”问题:
在上面,如果你点击第一个,你会得到“Index is: 0”,如果你点击最后一个,你会得到“Index is: 4”。这并没有,如果你使用的工作var
,而不是let
(你永远看“:5指数”)。
就像for-of
,for
循环在async
函数中运行良好。这是使用for
循环的较早示例:
for-in
for-in
不是用于遍历数组,而是用于遍历对象属性的名称。作为数组是对象这一事实的副产品,它似乎经常用于循环遍历数组,但它不仅循环遍历数组索引,还循环遍历对象的所有可枚举属性(包括继承的属性)。(也曾经是没有指定顺序;现在是[详情在这个其他答案中],但是即使现在指定了顺序,规则也很复杂,也有例外,依赖顺序不是最佳实践。)
for-in
数组上唯一真正的用例是:
仅查看第一个示例:for-in
如果您使用适当的保护措施,您可以使用访问那些稀疏数组元素:
// `a` is a sparse array
const a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (const name in a) {
if (Object.hasOwn(a, name) && // These checks are
/^0$|^[1-9]\d*$/.test(name) && // explained
name <= 4294967294 // below
) {
const element = a[name];
console.log(a[name]);
}
}
注意三项检查:
该对象具有该名称的自己的属性(不是它从其原型继承的属性;此检查也经常被编写为a.hasOwnProperty(name)
但 ES2022 添加了Object.hasOwn
它可以更可靠),以及
名称都是十进制数字(例如,正常的字符串形式,而不是科学记数法),以及
强制为数字时名称的值为 <= 2^32 - 2(即 4,294,967,294)。这个数字从何而来?它是规范中数组索引定义的一部分。其他数字(非整数、负数、大于 2^32 - 2 的数字)不是数组索引。它是 2^32 - 2的原因是这使得最大索引值低于 2^32 - 1,这是数组length
可以具有的最大值。(例如,数组的长度适合 32 位无符号整数。)
...尽管如此,大多数代码只进行hasOwnProperty
检查。
当然,您不会在内联代码中这样做。你会写一个实用程序函数。也许:
像for
,for-in
如果其中的工作需要串行完成,则在异步函数中效果很好。
for-of
隐式使用迭代器,为您完成所有 scut 工作。有时,您可能希望显式使用迭代器。它看起来像这样:
const a = ["a", "b", "c"];
const it = a.values(); // Or `const it = a[Symbol.iterator]();` if you like
let entry;
while (!(entry = it.next()).done) {
const element = entry.value;
console.log(element);
}
迭代器是与规范中的迭代器定义相匹配的对象。每次调用它的next
方法时,它都会返回一个新的结果对象。结果对象有一个属性 ,done
告诉我们它是否完成,还有一个属性value
包含该迭代的值。(done
如果是false
,value
则是可选的,如果是,则是可选的undefined
。)
你得到的东西value
取决于迭代器。阵列上,缺省迭代器提供每个阵列元素的值("a"
,"b"
,和"c"
在前面的例子)。数组还有其他三个返回迭代器的方法:
values()
:这是[Symbol.iterator]
返回默认迭代器的方法的别名。keys()
: 返回一个迭代器,它提供数组中的每个键(索引)。在上面的示例中,它将提供"0"
, then "1"
, then "2"
(是的,作为字符串)。entries()
: 返回一个提供[key, value]
数组的迭代器。由于迭代器对象在您调用 之前不会前进next
,因此它们在async
函数循环中运行良好。这是先前for-of
明确使用迭代器的示例:
除了真正的数组,还有类似数组的对象,它们有一个length
属性和全数字名称的属性:NodeList
instances、HTMLCollection
instances、arguments
object 等。我们如何遍历它们的内容?
上面的数组方法中至少有一些,可能是大部分甚至全部,同样适用于类似数组的对象:
使用for-of
(隐式使用迭代器)(ES2015+)
for-of
使用对象提供的迭代器(如果有)。这包括主机提供的对象(如 DOM 集合和列表)。例如,HTMLCollection
来自getElementsByXYZ
方法NodeList
的实例和来自querySelectorAll
两者的s 实例都支持迭代。(这是由 HTML 和 DOM 规范非常巧妙地定义的。基本上,任何具有length
和 索引访问的对象都是自动可迭代的。它不必被标记iterable
;这仅用于集合,除了可迭代之外,还支持forEach
,values
, keys
, 和entries
methods. NodeList
do; HTMLCollection
not, 但两者都是可迭代的。)
下面是一个循环遍历div
元素的例子:
使用forEach
及相关(ES5+)
on 的各种函数Array.prototype
是“有意通用的”,可以通过Function#call
( spec | MDN ) 或Function#apply
( spec | MDN )用于类数组对象。(如果您必须处理 IE8 或更早版本 [ouch],请参阅本答案末尾的“主机提供对象的警告”,但这对于模糊现代的浏览器来说不是问题。)
假设您想forEach
在 aNode
的childNodes
集合上使用(作为 ,本机HTMLCollection
没有forEach
)。你会这样做:
Array.prototype.forEach.call(node.childNodes, (child) => {
// Do something with `child`
});
(但请注意,您可以只使用for-of
on node.childNodes
。)
如果您打算经常这样做,您可能希望将函数引用的副本抓取到变量中以供重用,例如:
// (This is all presumably in a module or some scoping function)
const forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
// Then later...
forEach(node.childNodes, (child) => {
// Do something with `child`
});
使用简单的for
循环
也许很明显,一个简单的for
循环适用于类似数组的对象。
显式使用迭代器(ES2015+)
见#1。
您可能能够逃脱for-in
(使用安全措施),但是有了所有这些更合适的选项,就没有理由尝试了。
其他时候,您可能希望将类似数组的对象转换为真正的数组。做到这一点非常简单:
用 Array.from
Array.from
(规格) | (MDN)(ES2015+,但很容易填充)从一个类似数组的对象创建一个数组,可以选择首先通过映射函数传递条目。所以:
const divs = Array.from(document.querySelectorAll("div"));
...获取NodeList
fromquerySelectorAll
并从中创建一个数组。
如果您打算以某种方式映射内容,映射功能会很方便。例如,如果您想获取具有给定类的元素的标签名称数组:
// Typical use (with an arrow function):
const divs = Array.from(document.querySelectorAll(".some-class"), element => element.tagName);
// Traditional function (since `Array.from` can be polyfilled):
var divs = Array.from(document.querySelectorAll(".some-class"), function(element) {
return element.tagName;
});
使用扩展语法 ( ...
)
也可以使用 ES2015 的扩展语法。就像for-of
,这使用了对象提供的迭代器(参见上一节中的 #1):
const trueArray = [...iterableObject];
因此,例如,如果我们想将 a 转换NodeList
为一个真正的数组,使用展开语法这会变得非常简洁:
const divs = [...document.querySelectorAll("div")];
使用slice
数组的方法
我们可以使用slice
数组的方法,与上面提到的其他方法一样,它是“有意通用的”,因此可以与类似数组的对象一起使用,如下所示:
const trueArray = Array.prototype.slice.call(arrayLikeObject);
因此,例如,如果我们想将 aNodeList
转换为真正的数组,我们可以这样做:
const divs = Array.prototype.slice.call(document.querySelectorAll("div"));
(如果您仍然必须处理 IE8 [哎哟],将会失败;IE8 不允许您像this
那样使用主机提供的对象。)
如果您使用主机提供的类似数组的对象的Array.prototype
函数(例如,DOM 集合等由浏览器而不是 JavaScript 引擎提供),IE8 等过时的浏览器不一定会处理这种情况,因此如果您必须支持它们,请务必在您的目标环境中进行测试。但这对于模糊现代的浏览器来说不是问题。(对于非浏览器环境,自然会因环境而异。)
注意:这个答案已经过时了。对于更现代的方法,请查看array 上可用的方法。感兴趣的方法可能是:
在JavaScript 中迭代数组的标准方法是 vanilla循环for
:
var length = arr.length,
element = null;
for (var i = 0; i < length; i++) {
element = arr[i];
// Do something with element
}
但是请注意,这种方法仅适用于密集数组,并且每个索引都被一个元素占用。如果数组是稀疏的,那么使用这种方法可能会遇到性能问题,因为您将迭代数组中并不真正存在的许多索引。在这种情况下,for .. in
-loop 可能是一个更好的主意。但是,您必须使用适当的保护措施来确保仅对数组的所需属性(即数组元素)进行操作,因为for..in
-loop 也将在旧浏览器中枚举,或者如果附加属性定义为enumerable
.
在ECMAScript 5 中,数组原型上会有一个 forEach 方法,但在旧版浏览器中不支持。因此,为了能够始终如一地使用它,您必须有一个支持它的环境(例如,用于服务器端 JavaScript 的Node.js),或者使用“Polyfill”。然而,此功能的 Polyfill 是微不足道的,并且由于它使代码更易于阅读,因此它是一个很好的 polyfill。
如果您使用的是jQuery库,则可以使用jQuery.each:
$.each(yourArray, function(index, value) {
// do your stuff here
});
编辑 :
根据问题,用户想要使用 javascript 而不是 jquery 的代码,因此编辑是
var length = yourArray.length;
for (var i = 0; i < length; i++) {
// Do something with yourArray[i].
}
我认为这里的反向循环值得一提:
for (var i = array.length; i--; ) {
// process array[i]
}
len
变量,也不需要array.length
在每次迭代时进行比较,这两者都可能是一个微小的优化。array[i]
forEach()
了 ES6 的for ... of
.一些开发人员默认使用反向 for 循环,除非有充分的理由向前循环。
虽然性能提升通常微不足道,但它有点尖叫:
“对列表中的每一项都这样做,我不在乎顺序!”
然而在实践中是不实际的意图的可靠指标,因为它是从这些场合没有区别,当你做对井井有条,真的需要循环反向。因此,实际上需要另一种结构来准确表达“无关紧要”的意图,目前大多数语言(包括 ECMAScript)中都无法使用这种结构,但可以将其称为,例如forEachUnordered()
.
如果顺序无关紧要,而效率是一个问题(在游戏或动画引擎的最内层循环中),那么使用反向 for 循环作为首选模式可能是可以接受的。请记住,在现有代码中看到反向 for 循环并不一定意味着顺序无关紧要!
一般来说,对于更关注清晰度和安全性的更高级别的代码,我以前建议使用Array::forEach
作为循环的默认模式(尽管现在我更喜欢使用for..of
)。首选forEach
反向循环的原因是:
for
和while
循环)。然后,当您在代码中看到反向 for 循环时,这表明它被反转是有充分理由的(也许是上述原因之一)。看到传统的前向循环可能表明可以发生转移。
(如果对意图的讨论对您没有意义,那么您和您的代码可能会从 Crockford 的关于编程风格和您的大脑的讲座中受益。)
有一个关于是否辩论for..of
或者forEach()
是较好的:
为了获得最大的浏览器支持,迭代器for..of
需要一个 polyfill,使您的应用程序执行速度稍慢,下载速度稍大。
出于这个原因(并鼓励使用map
和filter
),一些前端风格指南for..of
完全禁止!
但是上述问题不适用于 Node.js 应用程序,for..of
现在已经很好地支持了 Node.js 应用程序。
就个人而言,我倾向于使用看起来最容易阅读的内容,除非性能或缩小已成为主要问题。因此,这些天我更喜欢使用for..of
代替forEach()
,但在适用时我将始终使用map
orfilter
或find
or some
。(为了同事,我很少用reduce
。)
for (var i = 0; i < array.length; i++) { ... } // Forwards
for (var i = array.length; i--; ) { ... } // Reverse
你会注意到这i--
是中间的子句(我们通常看到比较的地方),最后一个子句是空的(我们通常看到的地方i++
)。这意味着这i--
也用作继续的条件。至关重要的是,它在每次迭代之前执行和检查。
怎么开始array.length
不爆炸?
因为在每次迭代之前i--
运行,所以在第一次迭代时,我们实际上将访问避免数组越界项目出现任何问题的项目。array.length - 1
undefined
为什么它不在索引 0 之前停止迭代?
当条件i--
评估为 falsey 值(当它产生 0 时)时,循环将停止迭代。
诀窍是与 不同--i
,尾随i--
运算符递减i
但产生递减之前的值。您的控制台可以证明这一点:
> var i = 5; [i, i--, i];
[5, 5, 4]
所以在最后一次迭代中,i以前是1并且i--
表达式将其更改为0但实际上产生了1(真实),因此条件通过。在下一次迭代中,i--
将i更改为-1,但产生0(falsey),导致执行立即退出循环底部。
在传统的 for 循环中,i++
和++i
是可以互换的(正如 Douglas Crockford 指出的那样)。然而在相反的 for 循环中,因为我们的递减也是我们的条件表达式,i--
如果我们想处理索引 0 处的项目,我们必须坚持下去。
有些人喜欢在反向for
循环中画一个小箭头,并以眨眼结束:
for (var i = array.length; i --> 0 ;) {
感谢 WYL 向我展示了反向 for 循环的好处和可怕之处。
一些C风格的语言使用foreach
循环遍历枚举。在 JavaScript 中,这是通过for..in
循环结构完成的:
var index,
value;
for (index in obj) {
value = obj[index];
}
有一个问题。for..in
将遍历对象的每个可枚举成员及其原型上的成员。为了避免读取通过对象原型继承的值,只需检查该属性是否属于该对象:
for (i in obj) {
if (obj.hasOwnProperty(i)) {
//do stuff
}
}
此外,ECMAScript 5添加了一个forEach
方法Array.prototype
,可用于使用回调枚举数组(polyfill 位于文档中,因此您仍然可以将其用于旧浏览器):
arr.forEach(function (val, index, theArray) {
//do stuff
});
重要的是要注意Array.prototype.forEach
回调返回时不会中断false
。jQuery和Underscore.js提供了它们自己的变体each
来提供可以短路的循环。