在JavaScript中遍历数组的循环(对于每个)

发布时间 2023-10-09 21:19:36作者: 小满独家

内容来自 DOC https://q.houxu6.top/?s=在JavaScript中遍历数组的循环(对于每个)

我可以使用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"):

  1. 使用 for-of(隐式使用迭代器)(ES2015+)
  2. 使用 forEach 及相关方法(ES5+)
  3. 使用简单的 for 循环
  4. 正确使用 for-in(ES2015+)
  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`
    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-ofasync示例——请注意初始延迟,但随后所有文本立即出现,而不是等待:

显示代码片段

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定义了其他几个有用的“从头到尾遍历数组并执行操作”的函数,包括:

  • everyspec | MDN) - 当回调函数返回假值时停止循环
  • somespec | MDN) - 当回调函数返回真值时停止循环
  • filterspec | MDN) - 创建一个新的数组,其中包含回调函数返回真值的元素,忽略那些不返回真值的元素
  • mapspec | MDN) - 从回调函数返回的值中创建一个新的数组
  • reducespec | MDN) - 通过反复调用回调函数并传递先前的值来构建一个值;有关详细信息,请参阅规范
  • reduceRightspec | MDN) - 类似于reduce,但以降序而不是升序工作

forEach一样,如果您将async函数作为回调函数使用,那么这些方法都不会等待函数的promise解决。这意味着:

  • 使用async函数回调与everysomefilter一起从来不合适,因为它们会将返回的promise视为真值;它们不会等待promise解决然后使用满足值。
  • 在目标是为了将数组转换为promises数组的情况下(例如传递给Promise组合器函数(Promise.allPromise.racepromise.allSettledPromise.any)时,使用async函数回调通常是合适的。
  • 使用async函数回调与reducereduceRight一起很少合适,因为(再次)回调函数总是会返回一个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]);
    }
}

注意这三个检查:

  1. 对象拥有自己的属性名(不是从其原型继承的属性;这个检查通常也写作a.hasOwnProperty(name),但ES2022添加了Object.hasOwn,这可能更可靠),和
  2. 名称是所有十进制数字(例如,普通字符串形式,而不是科学计数法),和
  3. 当强制转换为数字时,名称的值应小于等于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,表示那次迭代的值。(如果donefalse,则value可选;如果valueundefined,则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对象等。我们如何遍历它们的内容?

使用上述选项中的大多数

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

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

for-of使用对象提供的迭代器(如果有的话)。这包括由主机提供的对象(如DOM集合和列表)。例如,getElementsByXYZ方法的HTMLCollection实例和querySelectorAllNodeList实例都支持迭代。(这由HTML和DOM规范非常隐式地定义。基本上,任何具有length和索引访问的对象都自动可迭代。它需要被标记为可迭代;那只用于除了支持forEachvalueskeysentries方法之外还支持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>
  1. 使用forEach及相关方法(ES5+)

Array.prototype上的各种函数是“有意为之的通用”的,可以通过Function#callspec | MDN)或Function#applyspec | MDN)在类数组对象上使用。如果您必须处理IE8或更早版本(哎呀),请参阅此答案末尾的“宿主提供的对象注意事项”,但它不会影响模糊现代浏览器。

假设您想在NodechildNodes集合上使用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`做一些操作
});
  1. 使用简单的for循环

显然,简单的for循环适用于类数组对象。

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

参见#1。

您可能可以逃脱使用for-in(带有保护措施),但是有了这些更合适的选项,没有理由尝试。

创建真实的数组

其他时候,你可能想要将类数组对象转换为真正的数组。这样做起来非常简单:

  1. 使用Array.from

Array.from (规格) | (MDN)(ES2015+,但可以轻松地实现polyfill)从类数组对象创建一个数组,可以选择首先通过映射函数传递条目。例如:

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

这将从querySelectorAllNodeList中创建一个数组。

映射函数在你想要以某种方式映射内容时非常有用。例如,如果你想获取具有给定类的元素的标签名数组:

// 典型用法(带有箭头函数):
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;
});
  1. 使用扩展语法(...

也可以使用ES2015的扩展语法[spread syntax]。像for-of一样,这个使用由对象提供的迭代器[iterator](参见上一节中的第1点):

const trueArray = [...iterableObject];

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

const divs = [...document.querySelectorAll("div")];
  1. 使用数组的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这样的过时浏览器并不一定以那种方式处理,所以如果你必须支持它们,请确保在你的目标环境中进行测试。但对于模糊现代浏览器来说这不是问题。(对于非浏览器环境,自然取决于环境。)