开发者问题收集

在 JavaScript 中对数组进行循环(for each)

2012-02-17
5349180

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

3个回答

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`...
      }
      }
      
  • 一些简单的“禁忌”:

    • 除非您使用 for-in 时采取了安全措施,或者至少知道它可能会对您造成影响,否则请勿使用它。
    • 如果您不使用 map 的返回值,请不要使用它
      (遗憾的是,有人在教 map [ spec / MDN ]就好像它是 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
  5. 明确使用迭代器(ES2015+)

(您可以在此处查看这些旧规范: ES5 ES2015 ,但两者都已被取代;当前编辑者的草稿始终在 此处 。)

详细信息:

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

ES2015 添加了 迭代器和可迭代对象 到 JavaScript。数组是可迭代的(字符串、 MapSet 以及 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 函数回调永远不适合 everysomefilter ,因为它们会将返回的承诺视为真值;它们 不会 等待承诺完成,然后使用履行值。
  • 使用 async 函数回调通常适用于 map 如果 目标是将某个数组转换为 承诺 数组,可能用于传递给承诺组合器函数之一( Promise.all Promise.race promise.allSettled ,或 Promise.any )。
  • 使用 async 函数回调很少适用于 reducereduceRight ,因为(再次)回调将始终返回一个承诺。但是有一种从使用 reduce 的数组构建承诺链的习惯用法 ( const promise = array.reduce((p, element) => p.then(/*...something using `element`...*/)); ),但通常在这些情况下, async 函数中的 for-offor 循环会更清晰且更容易调试。

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

请注意三个检查:

  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,这是数组 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 ,其中包含该迭代的值。(如果 donefalse ,则为可选;如果 valueundefined ,则为可选。)

对于 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 对象等。我们如何循环遍历它们的内容?

使用上面的大多数选项

至少使用上面的一些(可能是大多数甚至所有)数组方法同样适用于类似数组的对象:

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

    for-of 使用对象(如果有)提供的 迭代器 。这包括主机提供的对象(如 DOM 集合和列表)。例如,来自 getElementsByXYZ 方法的 HTMLCollection 实例和来自 querySelectorAllNodeList 实例都支持迭代。 (HTML 和 DOM 规范对此进行了 相当 巧妙的定义。基本上,任何具有 length 和索引访问的对象都是自动可迭代的。它 不必 标记为 iterable ;这仅适用于除了可迭代之外还支持 forEachvalueskeysentries 方法的集合。 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#call ( spec | MDN ) 或 Function#apply ( spec | MDN )。(如果您必须处理 IE8 或更早版本 [ouch],请参阅此答案末尾的“主机提供的对象注意事项”,但这对于现代浏览器来说不是问题。)

    假设您想在 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` 执行某些操作
    });
    
  2. 使用简单的 for 循环

    可能很明显,简单的 for 循环适用于类似数组的对象。

  3. 明确使用迭代器 (ES2015+)

    参见 #1。

可能 能够使用 for-in (有保障措施),但有了所有这些更合适的选项,就没有理由去尝试了。

创建一个真正的数组

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

  1. 使用 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;
    });
    
  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 [ouch],将会失败;IE8 不允许您像那样将主机提供的对象用作 this 。)

    <

    主机提供对象的注意事项

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

T.J. Crowder
2012-02-17

注意 :这个答案已经过时了。如需更现代的方法,请查看 数组上可用的方法 。感兴趣的方法可能包括:

  • 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。

PatrikAkerstrand
2012-02-17

如果您使用的是 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].
}
Poonam
2012-02-17