我可以使用JavaScript遍历数组中的所有条目吗?
TL;DR
-
你最好选择通常的方法是:
- 使用
for-of
循环(ES2015+ 只支持;规范 | MDN) - 简单且适用于async
。
for (const element of theArray) { // ...使用 `element`... }
- 使用
forEach
(ES5+ 只支持;规范 | MDN) - 不适用于async
,但请查看详细信息。
theArray.forEach(element => { // ...使用 `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`... } }
- 使用
-
一些快速“不要”:
- 不要使用
for-in
,除非你使用它带有保护措施或至少了解为什么它可能会伤害你。 - 不要使用
map
,如果你不打算使用它的返回值。
(不幸的是,有些人在教授map
[规范 / MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)],好像它是forEach
一样——但是正如我在博客上所写的,那不是它的用途。如果你不使用它创建的数组,就不要使用map
。) - 不要使用
forEach
,如果回调执行异步工作并且你想要让forEach
等待该工作完成(因为它不会)。
- 不要使用
但是还有更多可以探索的内容,请继续阅读...
JavaScript 对遍历数组和类似数组的对象具有强大的语义。我的回答分为两部分:适用于真正数组的选项,以及适用于只是类似数组的对象(例如 arguments
对象、其他可迭代对象(ES2015+)、DOM 集合等)的选项。
好的,让我们看一下我们的选项:
对于实际数组:
你有五个选项(两个基本上永远支持,一个是在 ECMAScript 5 中添加的 ["ES5"],另外两个是在 ECMAScript 2015 中添加的("ES2015",也称为 "ES6"):
- 使用
for-of
(隐式使用迭代器)(ES2015+) - 使用
forEach
及相关方法(ES5+) - 使用简单的
for
循环 - 正确使用
for-in
(ES2015+) - 显式使用迭代器(ES2015+)
(你可以在这里查看这些旧规范:ES5,ES2015,但两者都已被取代;当前编辑器草案始终在这里。)
细节:
1. 使用 for-of
(隐式使用迭代器)(ES2015+)
ES2015 向 JavaScript 添加了 迭代协议和可迭代对象。数组是可迭代的(字符串、Map
、Set
也是如此,以及稍后你会看到的 DOM 集合和列表)。可迭代对象为其值提供迭代器。新的 for-of
语句通过迭代器循环返回迭代器返回的值。
const a = ["a", "b", "c"];
for (const element of a) { // 如果你喜欢,可以使用 `let` 代替 `const`
console.log(element);
}
// a
// b
// c
没有比这更简单的了!在底层,这将从数组获取一个迭代器并循环通过迭代器返回的值。数组提供的迭代器提供数组元素的值,按顺序从开始到结束。
注意element
在每次循环迭代中的作用域;在循环结束后尝试使用element
会失败,因为它在循环体之外不存在。
从理论上讲,for-of
循环涉及几次函数调用(一次获取迭代器,然后一次从迭代器中获取每个值)。即使这是真的,也不用担心,现代JavaScript引擎中的函数调用成本非常低(在我对forEach
下面的关注之前,我一直很困扰;详细信息)(http://blog.niftysnippets.org/2012/02/foreach-and-runtime-cost.html)。但是此外,当处理诸如数组等本地迭代器的高性能代码时,JavaScript引擎会优化这些调用(将其消除)。
for-of
完全支持async
。如果您需要在循环体内按顺序完成工作(而不是并行),则可以在循环体中使用await
等待Promise解决后再继续。以下是一个简单的示例:
显示代码片段
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`省略,因为我们知道它永远不会拒绝
请注意每个单词在出现之前的延迟。
虽然只是编码风格的问题,但当遍历任何可迭代对象时,我首先会想到for-of
。
2. 使用forEach
及相关方法
在任何即使是稍微现代的环境(因此,不是IE8)中,只要您可以访问由ES5添加的Array
功能,就可以使用forEach
(规范 | 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解决后再继续。以下是使用forEach
而不是for-of
的async
示例——请注意初始延迟,但随后所有文本立即出现,而不是等待:
显示代码片段
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`省略,因为我们知道它永远不会拒绝
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视为真值;它们不会等待promise解决然后使用满足值。 - 在目标是为了将数组转换为promises数组的情况下(例如传递给Promise组合器函数(
Promise.all
,Promise.race
,promise.allSettled
或Promise.any
)时,使用async
函数回调通常是合适的。 - 使用
async
函数回调与reduce
或reduceRight
一起很少合适,因为(再次)回调函数总是会返回一个promise。但是有一种从使用reduce
的数组构建一系列promise的惯用法(const promise = array.reduce((p, element) => p.then(/*...something using
element...*/));
),但通常情况下,在这些情况下,使用async
函数的for
循环或for
循环会更清晰易懂,也更容易调试。
在ES2015之前,循环变量必须存在于包含的作用域中,因为var
只有函数级别的作用域,而不是块级作用域。但是正如你在上面的示例中所看到的,你可以在for
循环中使用let
来将变量范围限制在循环内。当你这样做时,每次循环迭代都会重新创建index
变量,这意味着在循环体内创建的闭包会保留对该特定迭代的index
的引用,从而解决了旧的“循环中的闭包”问题:
显示代码片段
// (从`querySelectorAll`获取的`NodeList`类似于数组)
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"。如果你使用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`省略了,因为我们知道他永远不会拒绝
4. 正确使用for-in
for-in
不是用于遍历数组,而是用于遍历对象属性的名称。它确实经常看起来像是作为数组的副产品而起作用,但事实并非如此;它不仅仅是遍历数组索引,而是遍历所有可枚举的属性(包括继承的属性)。(它曾经的顺序也不是特定的;现在细节在这个其他答案中有说明,但是即使顺序现在被指定了,规则也很复杂,存在例外,依赖顺序并不是最佳实践。)
数组上for-in
的唯一实际用例是:
- 它是一个稀疏数组,其中有很大的间隙,或者
- 你在数组对象上使用非元素属性,并且你想将它们包含在循环中
只看第一个示例:如果你使用适当的保护措施,你可以使用for-in
访问这些稀疏数组元素:
// `a`是一个稀疏数组
const a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (const name in a) {
if (Object.hasOwn(a, name) && // 这些检查是
/^0$|^[1-9]\d\*$/.test(name) && // 下面解释的
name <= 4294967294 //
) {
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,而2^32 - 1是数组
length
可以具有的最大值。(例如,数组的长度适合32位无符号整数。)
...尽管这么说,大多数代码只做hasOwnProperty
检查。
当然,你不会在内联代码中这样做。你会编写一个实用函数。也许:
显示代码片段
// 用于没有`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)) { // 人们通常只做这个检查
const message = messages[name];
await delay(400);
console.log(message);
}
}
}
showSlowly([
"So", "long", "and", "thanks", "for", "all", "the", "fish!"
]);
// `.catch`省略了,因为我们知道他永远不会拒绝
5. 显式使用迭代器(ES2015+)
for-of
隐式使用迭代器,为你完成所有的繁重工作。有时候,你可能想要显式地使用迭代器。它看起来像这样:
const a = ["a", "b", "c"];
const it = a.values(); // 或者如果你喜欢的话,可以使用`const it = a[Symbol.iterator]();`
let entry;
while (!(entry = it.next()).done) {
const element = entry.value;
console.log(element);
}
迭代器是符合规范中的迭代器定义的对象。它的next
方法每次调用时都会返回一个新的结果对象。结果对象有一个属性done
,告诉我们是否完成,以及一个属性value
,表示那次迭代的值。(如果done
为false
,则value
可选;如果value
为undefined
,则value
可选。)
你得到的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` 省略了,因为我们知道它永远不会拒绝
对于类数组对象(Array-Like Objects)
除了真正的数组,还有一些具有length
属性和全数字名称的属性的类数组对象,例如:NodeList
实例,HTMLCollection
实例,arguments
对象等。我们如何遍历它们的内容?
使用上述选项中的大多数
至少有一些,可能全部或甚至大部分上述数组方法同样适用于类数组对象:
- 使用
for-of
(隐式使用迭代器)(ES2015+)
for-of
使用对象提供的迭代器(如果有的话)。这包括由主机提供的对象(如DOM集合和列表)。例如,getElementsByXYZ
方法的HTMLCollection
实例和querySelectorAll
的NodeList
实例都支持迭代。(这由HTML和DOM规范非常隐式地定义。基本上,任何具有length
和索引访问的对象都自动可迭代。它不需要被标记为可迭代;那只用于除了支持forEach
,values
,keys
和entries
方法之外还支持forEach
的方法集合。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或更早版本(哎呀),请参阅此答案末尾的“宿主提供的对象注意事项”,但它不会影响模糊现代浏览器。
假设您想在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
(规格) | (MDN)(ES2015+,但可以轻松地实现polyfill)从类数组对象创建一个数组,可以选择首先通过映射函数传递条目。例如:
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的扩展语法[spread syntax]。像for-of
一样,这个使用由对象提供的迭代器[iterator](参见上一节中的第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 [哎呀],将会失败;IE8没有让你像这样使用主机提供的对象作为this
。)
宿主提供的对象注意事项
如果你使用宿主提供的数组类似对象(例如,浏览器而不是JavaScript引擎提供的DOM集合等),像IE8这样的过时浏览器并不一定以那种方式处理,所以如果你必须支持它们,请确保在你的目标环境中进行测试。但对于模糊现代浏览器来说这不是问题。(对于非浏览器环境,自然取决于环境。)
标签:遍历,const,迭代,JavaScript,element,forEach,数组,使用 From: https://www.cnblogs.com/xiaomandujia/p/17753183.html