在 JavaScript 中对数组进行循环(for each)
如何使用 JavaScript 循环遍历数组中的所有条目?
TL;DR
-
最好的选择 通常是
-
一个
for-of
循环(仅限 ES2015+; 规范 | MDN ) - 简单且异步
友好for (const element of theArray) { // ...use `element`... }
-
forEach
(仅限 ES5+; spec | MDN )(或其相关some
等)- 不异步
友好(但请参阅详细信息)theArray.forEach(element => { // ...use `element`... });
-
一个简单的老式
for
循环 -async
友好for (let index = 0; index < theArray.length; ++index) { const element = theArray[index]; // ...使用 `element`... }
-
(很少)
for-in
带有安全措施 -async
友好for (const propertyName in theArray) { if (/*...是数组元素属性(见下文)...*/) { const element = theArray[propertyName]; // ...使用`element`... } }
-
一个
-
一些简单的“禁忌”:
但还有 很多 值得探索,请继续阅读...
JavaScript 具有强大的语义来循环遍历数组和类似数组的对象。我把答案分成了两部分:真正的数组的选项,以及类似数组的选项,比如
arguments
对象、其他可迭代对象(ES2015+)、DOM 集合等等。
好的,让我们看看我们的选项:
对于实际数组
您有五个选项(两个基本上永远受支持,另一个由 ECMAScript 5 [“ES5”] 添加,另外两个在 ECMAScript 2015(“ES2015”,又名“ES6”)中添加):
-
使用
for-of
(隐式使用迭代器)(ES2015+) -
使用
forEach
和相关(ES5+) -
使用简单的
for
循环 -
正确使用
for-in
- 明确使用迭代器(ES2015+)
(您可以在此处查看这些旧规范: ES5 、 ES2015 ,但两者都已被取代;当前编辑者的草稿始终在 此处 。)
详细信息:
1. 使用
for-of
(隐式使用迭代器)(ES2015+)
ES2015 添加了
迭代器和可迭代对象
到 JavaScript。数组是可迭代的(字符串、
Map
和
Set
以及 DOM 集合和列表也是可迭代的,稍后您将看到)。可迭代对象为其值提供迭代器。新的
for-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
[下面] 让我很困扰,直到我研究它;
详细信息
)。但此外,JavaScript 引擎在处理数组等本机迭代器时会优化这些调用(在性能关键代码中)。
for-of
完全是
异步
友好的。如果您需要循环体中的工作以串行方式(而非并行方式)完成,则循环体中的
await
将等待承诺完成之后再继续。这是一个愚蠢的例子:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function showSlowly(messages) {
for (const message of messages) {
await delay(400);
console.log(message);
}
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch` omitted because we know it never rejects
请注意,每个单词出现之前都有延迟。
这是一个编码风格的问题,但
for-of
是我在循环遍历任何可迭代对象时首先想到的。
2. 使用
forEach
和相关
在任何甚至是模糊的现代环境(因此,不是 IE8)中,您可以访问 ES5 添加的
Array
功能,如果您只处理同步代码(或者您不需要在循环期间等待异步过程完成),则可以使用
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
不会
等待
该函数的承诺完成之后再继续。以下是
for-of
中的
async
示例,改用
forEach
——请注意初始延迟,但随后所有文本都会立即出现,而不是等待:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function showSlowly(messages) {
// INCORRECT, doesn't wait before continuing,
// doesn't handle promise rejections
messages.forEach(async message => {
await delay(400);
console.log(message);
});
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch` omitted because we know it never rejects
forEach
是“循环遍历所有”函数,但 ES5 定义了其他几个有用的“遍历数组并执行操作”函数,包括:
-
every
( spec | MDN ) - 在回调第一次返回时停止循环假值 -
some
( spec | MDN ) - 回调第一次返回真值时停止循环 -
filter
( spec | MDN ) - 创建一个新数组,其中包含回调返回真值的元素,省略不返回真值的元素 -
map
( spec | MDN ) - 根据回调返回的值创建一个新数组 -
reduce
( spec | MDN ) - 通过反复调用回调并传入先前的值来建立一个值;请参阅规范了解详情 -
reduceRight
( 规范 | MDN ) - 与reduce
类似,但以降序而非升序工作
与
forEach
一样,如果您使用
async
函数作为回调,则这些函数都不会等待函数的承诺完成。这意味着:
-
使用
async
函数回调永远不适合every
、some
和filter
,因为它们会将返回的承诺视为真值;它们 不会 等待承诺完成,然后使用履行值。 -
使用
async
函数回调通常适用于map
, 如果 目标是将某个数组转换为 承诺 数组,可能用于传递给承诺组合器函数之一(Promise.all
、Promise.race
、promise.allSettled
,或Promise.any
)。 -
使用
async
函数回调很少适用于reduce
或reduceRight
,因为(再次)回调将始终返回一个承诺。但是有一种从使用reduce
的数组构建承诺链的习惯用法 (const promise = array.reduce((p, element) => p.then(/*...something using `element`...*/));
),但通常在这些情况下,async
函数中的for-of
或for
循环会更清晰且更容易调试。
3.使用简单的
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
仅具有函数级作用域,而不是块级作用域。但正如您在上面的示例中所看到的,您可以在
for
中使用
let
将变量范围限定在循环中。当您这样做时,会为每个循环迭代重新创建
index
变量,这意味着在循环主体中创建的闭包会保留对该特定迭代的
index
的引用,这解决了旧的“循环中的闭包”问题:
// (The `NodeList` from `querySelectorAll` is array-like)
const divs = document.querySelectorAll("div");
for (let index = 0; index < divs.length; ++index) {
divs[index].addEventListener('click', e => {
console.log("Index is: " + index);
});
}
<div>zero</div>
<div>one</div>
<div>two</div>
<div>three</div>
<div>four</div>
在上面,如果单击第一个,您将获得“Index is: 0”,如果单击最后一个,您将获得“Index is: 4”。如果您使用
var
而不是
let
,则
不起作用
(您总是会看到“Index is: 5”)。
与
for-of
一样,
for
循环在
async
函数中运行良好。下面是使用
for
循环的早期示例:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function showSlowly(messages) {
for (let i = 0; i < messages.length; ++i) {
const message = messages[i];
await delay(400);
console.log(message);
}
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch` omitted because we know it never rejects
4.
正确
使用
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 小 1,这是数组
length
可以具有的最大值。 (例如,数组的长度适合 32 位无符号整数。)
...尽管如此,大多数代码仅执行
hasOwnProperty
检查。
当然,您不会在内联代码中执行此操作。您会编写一个实用函数。也许:
// Utility function for antiquated environments without `forEach`
const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
const rexNum = /^0$|^[1-9]\d*$/;
function sparseEach(array, callback, thisArg) {
for (const name in array) {
const index = +name;
if (hasOwn(a, name) &&
rexNum.test(name) &&
index <= 4294967294
) {
callback.call(thisArg, array[name], index, array);
}
}
}
const a = [];
a[5] = "five";
a[10] = "ten";
a[100000] = "one hundred thousand";
a.b = "bee";
sparseEach(a, (value, index) => {
console.log("Value at " + index + " is " + value);
});
与
for
一样,如果其中的工作需要按顺序完成,
for-in
可以在异步函数中很好地工作。
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function showSlowly(messages) {
for (const name in messages) {
if (messages.hasOwnProperty(name)) { // Almost always this is the only check people do
const message = messages[name];
await delay(400);
console.log(message);
}
}
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch` omitted because we know it never rejects
5. 明确使用迭代器(ES2015+)
for-of
隐式使用迭代器,为您完成所有繁琐的工作。有时,您可能希望
明确
地使用迭代器。它看起来像这样:
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);
}
迭代器是与规范中的 Iterator 定义匹配的对象。每次调用它的
next
方法时,都会返回一个新的
result 对象
。result 对象具有一个属性
done
,告诉我们它是否已完成,以及一个属性
value
,其中包含该迭代的值。(如果
done
为
false
,则为可选;如果
value
为
undefined
,则为可选。)
对于
value
,您获得的内容因迭代器而异。在数组上,默认迭代器提供每个数组元素的值(前面示例中的
“a”
、
“b”
和
“c”
)。数组还有另外三种返回迭代器的方法:
-
values()
:这是返回默认迭代器的[Symbol.iterator]
方法的别名。 -
keys()
:返回提供数组中每个键(索引)的迭代器。在上面的示例中,它将提供0
,然后是1
,然后是2
(作为数字,而不是字符串)。 (另请注意,在稀疏数组中,它 将 包含不存在元素的索引。) -
entries()
:返回提供[key, value]
数组的迭代器。
由于迭代器对象在调用
next
之前不会前进,因此它们在
async
函数循环中运行良好。以下是之前明确使用迭代器的
for-of
示例:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function showSlowly(messages) {
const it = messages.values()
while (!(entry = it.next()).done) {
await delay(400);
const element = entry.value;
console.log(element);
}
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch` omitted because we know it never rejects
对于类数组对象
除了真正的数组之外,还有
类数组
对象,它们具有
length
属性和全数字名称的属性:
NodeList
实例
、
HTMLCollection
实例
、
arguments
对象等。我们如何循环遍历它们的内容?
使用上面的大多数选项
至少使用上面的一些(可能是大多数甚至所有)数组方法同样适用于类似数组的对象:
-
使用
for-of
(隐式使用迭代器)(ES2015+)for-of
使用对象(如果有)提供的 迭代器 。这包括主机提供的对象(如 DOM 集合和列表)。例如,来自getElementsByXYZ
方法的HTMLCollection
实例和来自querySelectorAll
的NodeList
实例都支持迭代。 (HTML 和 DOM 规范对此进行了 相当 巧妙的定义。基本上,任何具有length
和索引访问的对象都是自动可迭代的。它 不必 标记为iterable
;这仅适用于除了可迭代之外还支持forEach
、values
、keys
和entries
方法的集合。NodeList
支持;HTMLCollection
不支持,但两者都是可迭代的。)以下是循环遍历
div
元素的示例:
const divs = document.querySelectorAll("div");
for (const div of divs) {
div.textContent = Math.random();
}
<div>zero</div>
<div>one</div>
<div>two</div>
<div>three</div>
<div>four</div>
-
使用
forEach
及相关 (ES5+)Array.prototype
上的各种函数是“有意通用的”,可通过Function#call
( spec | MDN ) 或Function#apply
( spec | MDN )。(如果您必须处理 IE8 或更早版本 [ouch],请参阅此答案末尾的“主机提供的对象注意事项”,但这对于现代浏览器来说不是问题。)假设您想在
Node
的childNodes
集合上使用forEach
(它是HTMLCollection
,本身没有forEach
)。您可以这样做:Array.prototype.forEach.call(node.childNodes, (child) => { // 使用 `child` 执行某些操作 });
(但请注意,您可以只在
node.childNodes
上使用for-of
。)如果您要经常这样做,您可能需要将函数引用的副本复制到变量中以供重用,例如:
//(这大概都在模块或某个作用域函数中) const forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); // 然后稍后... forEach(node.childNodes, (child) => { // 使用 `child` 执行某些操作 });
-
使用简单的
for
循环可能很明显,简单的
for
循环适用于类似数组的对象。 -
明确使用迭代器 (ES2015+)
参见 #1。
您
可能
能够使用
for-in
(有保障措施),但有了所有这些更合适的选项,就没有理由去尝试了。
创建一个真正的数组
其他时候,您可能希望将类似数组的对象转换为真正的数组。这样做非常简单:
-
使用
Array.from
Array.from
(spec) | (MDN) (ES2015+,但易于 polyfilled) 从类似数组的对象创建数组,可选择先将条目传递给映射函数。因此:const divs = Array.from(document.querySelectorAll("div"));
...从
querySelectorAll
获取NodeList
并从中创建一个数组。如果您要以某种方式映射内容,映射函数会非常方便。例如,如果您想要获取具有给定类的元素的标签名称数组:
// 典型用法(使用箭头函数): const divs = Array.from(document.querySelectorAll(".some-class"), element => element.tagName); // 传统函数(因为 `Array.from` 可以进行 polyfill): var divs = Array.from(document.querySelectorAll(".some-class"), function(element) { return element.tagName; });
-
使用扩展语法 (
...
)也可以使用 ES2015 的 扩展语法 。与
for-of
一样,它使用对象提供的 迭代器 (请参阅上一节中的 #1):const trueArray = [...iterableObject];
例如,如果我们想将
NodeList
转换为真正的数组,使用扩展语法会变得非常简洁:const divs = [...document.querySelectorAll("div")];
-
使用数组的
slice
方法我们可以使用数组的
slice
方法,该方法与上面提到的其他方法一样是“有意通用的”,因此可以与类似数组的对象一起使用,如下所示:const trueArray = Array.prototype.slice.call(arrayLikeObject);
因此,例如,如果我们想将
NodeList
转换为真正的数组,我们可以这样做:const divs = Array.prototype.slice.call(document.querySelectorAll("div"));
(如果您仍然需要处理 IE8 [ouch],将会失败;IE8 不允许您像那样将主机提供的对象用作
<this
。)主机提供对象的注意事项
如果您将
Array.prototype
函数与 主机提供 的数组类对象(例如,浏览器而非 JavaScript 引擎提供的 DOM 集合等)结合使用,那么像 IE8 这样的过时浏览器不一定会以这种方式处理,因此如果您必须支持它们,请务必在您的目标环境中进行测试。但对于现代浏览器来说这不是问题。(对于非浏览器环境,自然取决于环境。)
注意 :这个答案已经过时了。如需更现代的方法,请查看 数组上可用的方法 。感兴趣的方法可能包括:
- forEach
- map
- filter
- zip
- reduce
- every
- some
在
JavaScript
中迭代数组的标准方法是原始
for
循环:
var length = arr.length,
element = null;
for (var i = 0; i < length; i++) {
element = arr[i];
// Do something with element
}
但请注意,这种方法仅在您拥有密集数组且每个索引都被一个元素占用时才有效。如果数组是稀疏的,那么使用此方法可能会遇到性能问题,因为您将迭代数组中
实际上
不存在的大量索引。在这种情况下,使用
for .. in
循环可能是一个更好的主意。
但是
,您必须使用适当的保护措施来确保只对数组的所需属性(即数组元素)执行操作,因为
for..in
循环也将在旧版浏览器中枚举,或者如果其他属性定义为
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].
}