在 JavaScript 中的数组上的 For-each [英] For-each over an array in JavaScript

查看:22
本文介绍了在 JavaScript 中的数组上的 For-each的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何使用 JavaScript 遍历数组中的所有条目?

我以为是这样的:

forEach(数组中的实例)

其中 theArray 是我的数组,但这似乎不正确.

解决方案

TL;DR

  • 你最好的选择通常是

    • a for-of 循环(仅限 ES2015+;规范 | MDN) - 简单且async-友好

      for(数组的const元素){//...使用`元素`...}

    • forEach(仅限 ES5+;规范| MDN)(或其亲属一些 等) - not async-friendly(但请参阅详细信息)

      theArray.forEach(element => {//...使用`元素`...});

    • 一个简单的老式 for 循环 - async 友好

      for (let index = 0; index 

    • (很少) for-in 有保护措施 - async-friendly

      for (const propertyName in theArray) {if (/*...是一个数组元素属性(见下文)...*/) {const element = theArray[propertyName];//...使用`元素`...}}

  • 一些快速的不要":

    • 不要使用 for-in,除非您在使用时采取了保护措施,或者至少知道它为什么会咬到您.
    • 如果不使用 map 的返回值,请不要使用它.
      (遗憾的是有人在教 map [spec/MDN] 就好像它是 forEach — 但作为我在我的博客上写道,那不是它的用途.如果你不使用它创建的数组,不要使用 map.)
    • 不要使用 forEach 如果回调执行异步工作并且您希望 forEach 等到该工作完成(因为它不会).

但是还有很多需要探索,继续阅读...


JavaScript 具有强大的语义来循环遍历数组和类数组对象.我将答案分为两部分:真正数组的选项,以及只是数组like 的选项,例如 arguments 对象,其他可迭代对象(ES2015+)、DOM 集合等.

好的,让我们看看我们的选择:

对于实际数组

您有五个选项(两个基本上永久支持,另一个由 ECMAScript 5 [ES5"] 添加,另外两个在 ECMAScript 2015(ES2015",又名ES6")中添加:

  1. 使用 for-of(隐式使用迭代器)(ES2015+)
  2. 使用 forEach 和相关 (ES5+)
  3. 使用简单的for循环
  4. 使用for-in 正确
  5. 显式使用迭代器 (ES2015+)

(您可以在此处查看旧规范:ES5ES2015,但是两者都被取代了;当前编辑的草稿总是这里.)

详情:

1.使用 for-of(隐式使用迭代器)(ES2015+)

ES2015 添加了迭代器和可迭代对象 到 JavaScript.数组是可迭代的(字符串、MapSet 以及 DOM 集合和列表也是如此,稍后您将看到).可迭代对象为其值提供迭代器.新的 for-of 语句循环遍历迭代器返回的值:

const a = ["a", "b", "c"];for (const element of a) {//如果愿意,可以使用 `let` 代替 `const`控制台日志(元素);}//一种//乙//c

没有比这更简单的了!在幕后,它从数组中获取一个迭代器并遍历迭代器返回的值.数组提供的迭代器提供数组元素的值,从头到尾.

注意 element 是如何限定于每次循环迭代的;尝试在循环结束后使用 element 会失败,因为它不存在于循环体之外.

理论上,for-of 循环涉及多个函数调用(一个用于获取迭代器,然后一个用于从中获取每个值).即使这是真的,也没有什么可担心的,现代 JavaScript 引擎中的函数调用非常便宜(它让我为 forEach [下面]直到我查看它;详细信息).但此外,在处理数组等原生迭代器时,JavaScript 引擎会优化这些调用(在性能关键代码中).

for-of 完全是 async 友好的.如果您需要循环体中的工作以串行方式(而非并行方式)完成,则循环体中的 await 将在继续之前等待承诺解决.这是一个愚蠢的例子:

函数延迟(毫秒){返回新的承诺(解决 => {设置超时(解析,毫秒);});}异步函数 showSlowly(messages) {for(消息的常量消息){等待延迟(400);控制台日志(消息);}}慢慢显示([所以",长",和",谢谢",为了",所有",那个",鱼!"]);//`.catch` 省略,因为我们知道它永远不会拒绝

注意单词出现在每个单词之前的延迟.

这是一个编码风格的问题,但是 for-of 是我在遍历任何可迭代的东西时首先要达到的.

2.使用 forEach 和相关的

在任何可以访问 ES5 添加的 Array 功能的甚至模糊的现代环境(因此,不是 IE8)中,您都可以使用 forEach(规范 | MDN) 如果您只处理同步代码(或者您不需要在循环):

const a = ["a", "b", "c"];a.forEach((元素) => {控制台日志(元素);});

forEach 接受一个回调函数,并且可以选择在调用该回调时用作 this 的值(上面未使用).为数组中的每个元素调用回调,按顺序跳过稀疏数组中不存在的元素.虽然我只使用了上面的一个参数,但回调函数是用三个参数调用的:该迭代的元素、该元素的索引以及对您正在迭代的数组的引用(以防您的函数还没有它方便).

for-of 一样,forEach 的优点是您不必在包含范围内声明索引和值变量;在这种情况下,它们作为迭代函数的参数提供,并且很好地限定在该迭代中.

for-of不同,forEach的缺点是它不理解async函数和await.如果您使用 async 函数作为回调函数,forEach 不会等待该函数的承诺解决后再继续.这是 for-of 中的 async 示例,使用 forEach 代替 - 请注意初始延迟是如何出现的,但随后所有文本都会立即出现而不是等待:

函数延迟(毫秒){返回新的承诺(解决 => {设置超时(解析,毫秒);});}异步函数 showSlowly(messages) {//不正确,在继续之前不等待,//不处理承诺拒绝消息.forEach(异步消息=> {等待延迟(400);控制台日志(消息);});}慢慢显示([所以",长",和",谢谢",为了",所有",那个",鱼!"]);//`.catch` 省略,因为我们知道它永远不会拒绝

forEach 是循环遍历它们";函数,但 ES5 定义了其他几个有用的按你的方式处理数组并做事";功能,包括:

  • every (spec | MDN) - 第一次停止循环回调返回一个假值
  • some (规范 | MDN) - 第一次停止循环回调返回一个真值
  • filter(规范 | MDN) - 创建一个包含元素的新数组回调返回一个真值,省略那些没有的值
  • map(规范 | MDN) - 从回调返回的值
  • reduce (规范 | MDN) - 通过反复建立价值调用回调,传入先前的值;详情请参阅规范
  • reduceRight(规范 | MDN) - 就像 reduce,但按降序而不是升序工作

forEach 一样,如果您使用 async 函数作为回调,则这些函数都不会等待函数的承诺完成.这意味着:

  • everysomefilter 使用 async 函数回调永远不合适,因为它们会处理返回的 promise 就好像它是一个真实的值一样;他们等待承诺完成,然后使用履行价值.
  • 使用 async 函数回调通常适用于 mapif 的目标是将某个数组转换为 mapem>promises,也许是为了传递给 promise 组合器函数之一(Promise.all, Promise.race, promise.allSettledPromise.any).
  • 使用 async 函数回调很少适用于 reducereduceRight,因为(再次)回调将始终返回一个承诺.但是有一种习惯用法是从使用 reduce (const promise = array.reduce((p, element) => p.then(/*...something 使用 `element`...*/));),但通常在这些情况下 for-offor 循环在 async 功能会更清晰,更容易调试.

3.使用简单的 for 循环

有时老方法是最好的:

const a = ["a", "b", "c"];for (let index = 0; index < a.length; ++index) {const 元素 = a[索引];控制台日志(元素);}

如果数组的长度在循环过程中不会改变,并且它在对性能高度敏感的代码中,一个稍微复杂一点的预先获取长度的版本可能是一个微小 快一点:

const a = ["a", "b", "c"];for (let index = 0, len = a.length; index 

和/或倒数:

const a = ["a", "b", "c"];for (let index = a.length - 1; index >= 0; --index) {const 元素 = a[索引];控制台日志(元素);}

但是对于现代 JavaScript 引擎,您很少需要勉强维持最后一点力气.

在 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);});}

<div>零</div><div>一个</div><div>二</div><div>三</div><div>四</div>

在上面,你得到索引是:0";如果您单击第一个和索引为:4"如果你点击最后一个.如果您使用 var 而不是 let(您总是会看到Index is: 5"),这不起作用.

for-of 一样,for 循环在async 函数中运行良好.这是前面使用 for 循环的示例:

函数延迟(毫秒){返回新的承诺(解决 => {设置超时(解析,毫秒);});}异步函数 showSlowly(messages) {for (let i = 0; i 

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) &&//解释名称 <= 4294967294//下面){const 元素 = a[名称];console.log(a[name]);}}

注意三个检查:

  1. 该对象具有该名称的自己的属性(不是从其原型继承的;此检查通常也写为 a.hasOwnProperty(name)code> 但 ES2022 添加了 Object.hasOwn 可以更可靠),和

  2. 名称都是十进制数字(例如,正常的字符串形式,而不是科学记数法),并且

  3. 名称在强制为数字时的值为 <= 2^32 - 2(即 4,294,967,294).这个数字从何而来?它是数组索引定义的一部分在规范中.其他数字(非整数、负数、大于 2^32 - 2 的数字)不是数组索引.它是 2^32 - 2 的原因是,这使得最大索引值比 2^32 - 1 小一个,后者是数组的长度的最大值 可以有.(例如,数组的长度适合 32 位无符号整数.)

...尽管如此,大多数代码只做 hasOwnProperty 检查.

当然,您不会在内联代码中这样做.你会写一个实用程序函数.也许:

//没有 `forEach` 的过时环境的实用函数const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);const rexNum =/^0$|^[1-9]\d*$/;函数 sparseEach(数组,回调,thisArg){for(数组中的常量名){常量索引 = + 名称;if (hasOwn(a, name) &&rexNum.test(name) &&指数 <= 4294967294){callback.call(thisArg, array[name], index, array);}}}const a = [];a[5] = "五";a[10] = "十";a[100000] = "十万";a.b = "蜜蜂";sparseEach(a, (value, index) => {console.log("" + index + " 处的值是 " + 值);});

for 一样,for-in 在异步函数中运行良好,如果其中的工作需要串行完成.

函数延迟(毫秒){返回新的承诺(解决 => {设置超时(解析,毫秒);});}异步函数 showSlowly(messages) {for(消息中的常量名称){if (messages.hasOwnProperty(name)) {//这几乎总是人们做的唯一检查常量消息 = 消息 [名称];等待延迟(400);控制台日志(消息);}}}慢慢显示([所以",长",和",谢谢",为了",所有",那个",鱼!"]);//`.catch` 省略,因为我们知道它永远不会拒绝

5.显式使用迭代器 (ES2015+)

for-of 隐式使用迭代器,为您完成所有的 scut 工作.有时,您可能希望显式使用迭代器.它看起来像这样:

const a = ["a", "b", "c"];const it = a.values();//或者 `const it = a[Symbol.iterator]();` 如果你喜欢让进入;while (!(entry = it.next()).done) {const 元素 = entry.value;控制台日志(元素);}

迭代器是与规范中的迭代器定义相匹配的对象.它的 next 方法每次调用时都会返回一个新的 result 对象.结果对象有一个属性,done,告诉我们它是否已经完成,还有一个属性value 带有该迭代的值.(done 是可选的,如果它是 falsevalue 是可选的,如果它是 undefined.)>

您从 value 获得的内容因迭代器而异.在数组上,默认迭代器提供每个数组元素的值(a"b"c"在前面的例子中).数组还有其他三个返回迭代器的方法:

  • values():这是返回默认迭代器的 [Symbol.iterator] 方法的别名.
  • keys():返回一个迭代器,它提供数组中的每个键(索引).在上面的示例中,它将提供 "0",然后是 "1",然后是 "2"(是的,作为字符串).
  • entries():返回一个提供[key, value]数组的迭代器.

由于迭代器对象在您调用 next 之前不会前进,因此它们在 async 函数循环中运行良好.这是之前显式使用迭代器的 for-of 示例:

函数延迟(毫秒){返回新的承诺(解决 => {设置超时(解析,毫秒);});}异步函数 showSlowly(messages) {const it = messages.values()while (!(entry = it.next()).done) {等待延迟(400);const 元素 = entry.value;控制台日志(元素);}}慢慢显示([所以",长",和",谢谢",为了",所有",那个",鱼!"]);//`.catch` 省略,因为我们知道它永远不会拒绝

对于类数组对象

除了真正的数组,还有类似数组的对象具有 length 属性和具有全数字名称的属性:NodeList 实例HTMLCollection 实例arguments 对象等.我们如何遍历它们的内容?

使用上面的大部分选项

上述数组方法中的至少一些,可能是大部分甚至全部方法同样适用于类似数组的对象:

  1. 使用for-of(隐式使用迭代器)(ES2015+)

    for-of 使用 对象提供的迭代器(如果有的话).这包括主机提供的对象(如 DOM 集合和列表).例如,来自 getElementsByXYZ 方法的 HTMLCollection 实例和来自 querySelectorAllNodeList 实例都支持迭代.(这是由 HTML 和 DOM 规范相当巧妙地定义的.基本上,任何具有length 和索引访问的对象都是自动可迭代的.它不是 必须被标记为 iterable;它仅用于除了可迭代之外还支持 forEachvalues 的集合keysentries 方法.NodeList 有;HTMLCollection 没有,但两者都是可迭代的.)

    这是一个循环遍历 div 元素的例子:

const divs = document.querySelectorAll("div");for (const div of div) {div.textContent = Math.random();}

<div>零</div><div>一个</div><div>二</div><div>三</div><div>四</div>

  1. 使用 forEach 和相关 (ES5+)

    Array.prototype 上的各种函数是有意通用的";并且可以通过 Function#call (spec | MDN) 或 Function#apply(spec |MDN).(如果您必须处理 IE8 或更早版本 [哎哟],请参阅本答案末尾的主机提供对象的注意事项",但这对于模糊现代的浏览器来说不是问题.)

    假设您想在 NodechildNodes 集合(它是一个 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` 做一些事情});

  2. 使用简单的for循环

    也许很明显,一个简单的 for 循环适用于类似数组的对象.

  3. 显式使用迭代器 (ES2015+)

    见#1.

可能能够摆脱for-in(有保护措施),但是有了所有这些更合适的选项,没有理由尝试.

创建一个真正的数组

其他时候,您可能希望将类似数组的对象转换为真正的数组.做到这一点非常简单:

  1. 使用Array.from

    Array.from (规范) |(MDN)(ES2015+,但易于填充)从类似数组的对象创建一个数组,可以选择首先通过映射函数传递条目.所以:

    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) {返回元素.tagName;});

  2. 使用扩展语法(...)

    也可以使用 ES2015 的 传播语法.与 for-of 一样,它使用 对象提供的迭代器(参见上一节中的#1):

    const trueArray = [...iterableObject];

    例如,如果我们想将 NodeList 转换为真正的数组,使用扩展语法,这变得非常简洁:

    const divs = [...document.querySelectorAll(div")];

  3. 使用数组的slice方法

    我们可以使用slice 方法数组,与上面提到的其他方法一样是有意通用的".因此可以与类似数组的对象一起使用,如下所示:

    const trueArray = Array.prototype.slice.call(arrayLikeObject);

    例如,如果我们想将 NodeList 转换为真正的数组,我们可以这样做:

    const divs = Array.prototype.slice.call(document.querySelectorAll(div"));

    (如果你仍然要处理 IE8 [哎哟],会失败;IE8 不允许你像 this 那样使用主机提供的对象.)

注意宿主提供的对象

如果您将 Array.prototype 函数与主机提供的 类数组对象(例如,DOM 集合等由浏览器而不是 JavaScript 引擎提供)一起使用,像 IE8 这样过时的浏览器不一定会处理这种情况,因此如果您必须支持它们,请务必在您的目标环境中进行测试.但这对于模糊现代的浏览器来说不是问题.(对于非浏览器环境,自然会依赖环境.)

How can I loop through all the entries in an array using JavaScript?

I thought it was something like this:

forEach(instance in theArray)

Where theArray is my array, but this seems to be incorrect.

解决方案

TL;DR

  • Your best bets are usually

    • a for-of loop (ES2015+ only; spec | MDN) - simple and async-friendly

      for (const element of theArray) {
          // ...use `element`...
      }
      

    • forEach (ES5+ only; spec | MDN) (or its relatives some and such) - not async-friendly (but see details)

      theArray.forEach(element => {
          // ...use `element`...
      });
      

    • a simple old-fashioned for loop - async-friendly

      for (let index = 0; index < theArray.length; ++index) {
          const element = theArray[index];
          // ...use `element`...
      }
      

    • (rarely) for-in with safeguards - async-friendly

      for (const propertyName in theArray) {
          if (/*...is an array element property (see below)...*/) {
              const element = theArray[propertyName];
              // ...use `element`...
          }
      }
      

  • Some quick "don't"s:

    • Don't use for-in unless you use it with safeguards or are at least aware of why it might bite you.
    • Don't use map if you're not using its return value.
      (There's sadly someone out there teaching map [spec / MDN] as though it were forEach — but as I write on my blog, that's not what it's for. If you aren't using the array it creates, don't use map.)
    • Don't use forEach if the callback does asynchronous work and you want the forEach to wait until that work is done (because it won't).

But there's lots more to explore, read on...


JavaScript has powerful semantics for looping through arrays and array-like objects. I've split the answer into two parts: Options for genuine arrays, and options for things that are just array-like, such as the arguments object, other iterable objects (ES2015+), DOM collections, and so on.

Okay, let's look at our options:

For Actual Arrays

You have five options (two supported basically forever, another added by ECMAScript 5 ["ES5"], and two more added in ECMAScript 2015 ("ES2015", aka "ES6"):

  1. Use for-of (use an iterator implicitly) (ES2015+)
  2. Use forEach and related (ES5+)
  3. Use a simple for loop
  4. Use for-in correctly
  5. Use an iterator explicitly (ES2015+)

(You can see those old specs here: ES5, ES2015, but both have been superceded; the current editor's draft is always here.)

Details:

1. Use for-of (use an iterator implicitly) (ES2015+)

ES2015 added iterators and iterables to JavaScript. Arrays are iterable (so are strings, Maps, and Sets, as well as DOM collections and lists, as you'll see later). Iterable objects provide iterators for their values. The new for-of statement loops through the values returned by an iterator:

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

It doesn't get simpler than that! Under the covers, that gets an iterator from the array and loops through the values the iterator returns. The iterator provided by arrays provides the values of the array elements, in order beginning to end.

Notice how element is scoped to each loop iteration; trying to use element after the end of the loop would fail because it doesn't exist outside the loop body.

In theory, a for-of loop involves several function calls (one to get the iterator, then one to get each value from it). Even when that's true, it's nothing to worry about, function calls are very cheap in modern JavaScript engines (it bothered me for forEach [below] until I looked into it; details). But additionally, JavaScript engines optimize those calls away (in performance-critical code) when dealing with native iterators for things like arrays.

for-of is entirely async-friendly. If you need the work in a loop body to be done in series (not in parallel), an await in the loop body will wait for the promise to settle before continuing. Here's a silly example:

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

Note how the words appear with a delay before each one.

It's a matter of coding style, but for-of is the first thing I reach for when looping through anything iterable.

2. Use forEach and related

In any even vaguely-modern environment (so, not IE8) where you have access to the Array features added by ES5, you can use forEach (spec | MDN) if you're only dealing with synchronous code (or you don't need to wait for an asynchronous process to finish during the loop):

const a = ["a", "b", "c"];
a.forEach((element) => {
    console.log(element);
});

forEach accepts a callback function and, optionally, a value to use as this when calling that callback (not used above). The callback is called for each element in the array, in order, skipping non-existent elements in sparse arrays. Although I only used one parameter above, the callback is called with three arguments: The element for that iteration, the index of that element, and a reference to the array you're iterating over (in case your function doesn't already have it handy).

Like for-of, forEach has the advantage that you don't have to declare indexing and value variables in the containing scope; in this case, they're supplied as arguments to the iteration function, and so nicely scoped to just that iteration.

Unlike for-of, forEach has the disadvantage that it doesn't understand async functions and await. If you use an async function as the callback, forEach does not wait for that function's promise to settle before continuing. Here's the async example from for-of using forEach instead — notice how there's an initial delay, but then all the text appears right away instead of waiting:

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 is the "loop through them all" function, but ES5 defined several other useful "work your way through the array and do things" functions, including:

  • every (spec | MDN) - stops looping the first time the callback returns a falsy value
  • some (spec | MDN) - stops looping the first time the callback returns a truthy value
  • filter (spec | MDN) - creates a new array including elements where the callback returns a truthy value, omitting the ones where it doesn't
  • map (spec | MDN) - creates a new array from the values returned by the callback
  • reduce (spec | MDN) - builds up a value by repeatedly calling the callback, passing in previous values; see the spec for the details
  • reduceRight (spec | MDN) - like reduce, but works in descending rather than ascending order

As with forEach, if you use an async function as your callback, none of those waits for the function's promise to settle. That means:

  • Using an async function callback is never appropriate with every, some, and filter since they will treat the returned promise as though it were a truthy value; they don't wait for the promise to settle and then use the fulfillment value.
  • Using an async function callback is often appropriate with map, if the goal is to turn an array of something into an array of promises, perhaps for passing to one of the promise combinator functions (Promise.all, Promise.race, promise.allSettled, or Promise.any).
  • Using an async function callback is rarely appropriate with reduce or reduceRight, because (again) the callback will always return a promise. But there is an idiom of building a chain of promises from an array that uses reduce (const promise = array.reduce((p, element) => p.then(/*...something using `element`...*/));), but usually in those cases a for-of or for loop in an async function will be clearer and easier to debug.

3. Use a simple for loop

Sometimes the old ways are the best:

const a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
    const element = a[index];
    console.log(element);
}

If the length of the array won't change during the loop, and it's in highly performance-sensitive code, a slightly more complicated version grabbing the length up front might be a tiny bit faster:

const a = ["a", "b", "c"];
for (let index = 0, len = a.length; index < len; ++index) {
    const element = a[index];
    console.log(element);
}

And/or counting backward:

const a = ["a", "b", "c"];
for (let index = a.length - 1; index >= 0; --index) {
    const element = a[index];
    console.log(element);
}

But with modern JavaScript engines, it's rare you need to eke out that last bit of juice.

Before ES2015, the loop variable had to exist in the containing scope, because var only has function-level scope, not block-level scope. But as you saw in the examples above, you can use let within the for to scope the variables to just the loop. And when you do that, the index variable is recreated for each loop iteration, meaning closures created in the loop body keep a reference to the index for that specific iteration, which solves the old "closures in loops" problem:

// (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>

In the above, you get "Index is: 0" if you click the first and "Index is: 4" if you click the last. This does not work if you use var instead of let (you'd always see "Index is: 5").

Like for-of, for loops work well in async functions. Here's the earlier example using a for loop:

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. Use for-in correctly

for-in isn't for looping through arrays, it's for looping through the names of an object's properties. It does often seem to work for looping through arrays as a by-product of the fact that arrays are objects, but it doesn't just loop through the array indexes, it loops through all enumerable properties of the object (including inherited ones). (It also used to be that the order wasn't specified; it is now [details in this other answer], but even though the order is specified now, the rules are complex, there are exceptions, and relying on the order is not best practice.)

The only real use cases for for-in on an array are:

  • It's a sparse array with massive gaps in it, or
  • You're using non-element properties on the array object and you want to include them in the loop

Looking only at that first example: You can use for-in to visit those sparse array elements if you use appropriate safeguards:

// `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]);
    }
}

Note the three checks:

  1. That the object has its own property by that name (not one it inherits from its prototype; this check is also often written as a.hasOwnProperty(name) but ES2022 adds Object.hasOwn which can be more reliable), and

  2. That the name is all decimal digits (e.g., normal string form, not scientific notation), and

  3. That the name's value when coerced to a number is <= 2^32 - 2 (which is 4,294,967,294). Where does that number come from? It's part of the definition of an array index in the specification. Other numbers (non-integers, negative numbers, numbers greater than 2^32 - 2) are not array indexes. The reason it's 2^32 - 2 is that that makes the greatest index value one lower than 2^32 - 1, which is the maximum value an array's length can have. (E.g., an array's length fits in a 32-bit unsigned integer.)

...although with that said, most code only does the hasOwnProperty check.

You wouldn't do that in inline code, of course. You'd write a utility function. Perhaps:

// 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);
});

Like for, for-in works well in asynchronous functions if the work within it needs to be done in series.

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. Use an iterator explicitly (ES2015+)

for-of uses an iterator implicitly, doing all the scut work for you. Sometimes, you might want to use an iterator explicitly. It looks like this:

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);
}

An iterator is an object matching the Iterator definition in the specification. Its next method returns a new result object each time you call it. The result object has a property, done, telling us whether it's done, and a property value with the value for that iteration. (done is optional if it would be false, value is optional if it would be undefined.)

What you get for value varies depending on the iterator. On arrays, the default iterator provides the value of each array element ("a", "b", and "c" in the example earlier). Arrays also have three other methods that return iterators:

  • values(): This is an alias for the [Symbol.iterator] method that returns the default iterator.
  • keys(): Returns an iterator that provides each key (index) in the array. In the example above, it would provide "0", then "1", then "2" (yes, as strings).
  • entries(): Returns an iterator that provides [key, value] arrays.

Since iterator objects don't advance until you call next, they work well in async function loops. Here's the earlier for-of example using the iterator explicitly:

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

For Array-Like Objects

Aside from true arrays, there are also array-like objects that have a length property and properties with all-digits names: NodeList instances, HTMLCollection instances, the arguments object, etc. How do we loop through their contents?

Use most of the options above

At least some, and possibly most or even all, of the array approaches above apply equally well to array-like objects:

  1. Use for-of (use an iterator implicitly) (ES2015+)

    for-of uses the iterator provided by the object (if any). That includes host-provided objects (like DOM collections and lists). For instance, HTMLCollection instances from getElementsByXYZ methods and NodeLists instances from querySelectorAll both support iteration. (This is defined quite subtly by the HTML and DOM specifications. Basically, any object with length and indexed access is automatically iterable. It doesn't have to be marked iterable; that is used only for collections that, in addition to being iterable, support forEach, values, keys, and entries methods. NodeList does; HTMLCollection doesn't, but both are iterable.)

    Here's an example of looping through div elements:

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>

  1. Use forEach and related (ES5+)

    The various functions on Array.prototype are "intentionally generic" and can be used on array-like objects via Function#call (spec | MDN) or Function#apply (spec | MDN). (If you have to deal with IE8 or earlier [ouch], see the "Caveat for host-provided objects" at the end of this answer, but it's not an issue with vaguely-modern browsers.)

    Suppose you wanted to use forEach on a Node's childNodes collection (which, being an HTMLCollection, doesn't have forEach natively). You'd do this:

    Array.prototype.forEach.call(node.childNodes, (child) => {
        // Do something with `child`
    });
    

    (Note, though, that you could just use for-of on node.childNodes.)

    If you're going to do that a lot, you might want to grab a copy of the function reference into a variable for reuse, e.g.:

    // (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`
    });
    

  2. Use a simple for loop

    Perhaps obviously, a simple for loop works for array-like objects.

  3. Use an iterator explicitly (ES2015+)

    See #1.

You may be able to get away with for-in (with safeguards), but with all of these more appropriate options, there's no reason to try.

Create a true array

Other times, you may want to convert an array-like object into a true array. Doing that is surprisingly easy:

  1. Use Array.from

    Array.from (spec) | (MDN) (ES2015+, but easily polyfilled) creates an array from an array-like object, optionally passing the entries through a mapping function first. So:

    const divs = Array.from(document.querySelectorAll("div"));
    

    ...takes the NodeList from querySelectorAll and makes an array from it.

    The mapping function is handy if you were going to map the contents in some way. For instance, if you wanted to get an array of the tag names of the elements with a given class:

    // 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;
    });
    

  2. Use spread syntax (...)

    It's also possible to use ES2015's spread syntax. Like for-of, this uses the iterator provided by the object (see #1 in the previous section):

    const trueArray = [...iterableObject];
    

    So for instance, if we want to convert a NodeList into a true array, with spread syntax this becomes quite succinct:

    const divs = [...document.querySelectorAll("div")];
    

  3. Use the slice method of arrays

    We can use the slice method of arrays, which like the other methods mentioned above is "intentionally generic" and so can be used with array-like objects, like this:

    const trueArray = Array.prototype.slice.call(arrayLikeObject);
    

    So for instance, if we want to convert a NodeList into a true array, we could do this:

    const divs = Array.prototype.slice.call(document.querySelectorAll("div"));
    

    (If you still have to handle IE8 [ouch], will fail; IE8 didn't let you use host-provided objects as this like that.)

Caveat for host-provided objects

If you use Array.prototype functions with host-provided array-like objects (for example, DOM collections and such provided by the browser rather than the JavaScript engine), obsolete browsers like IE8 didn't necessarily handle that way, so if you have to support them, be sure to test in your target environments. But it's not an issue with vaguely-modern browsers. (For non-browser environments, naturally it'll depend on the environment.)

这篇关于在 JavaScript 中的数组上的 For-each的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆