在 JavaScript 中通过数组进行

2022-08-29 21:44:08

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


答案 1

TL;DR

  • 你最好的选择通常是

    • 一个循环(仅限ES2015 +;规格|MDN)- 简单友好for-ofasync
      for (const element of theArray) {
          // ...use `element`...
      }
      
    • forEach(仅限 ES5+;规格|MDN)(或其亲戚等) - 友好(但查看详细信息)someasync
      theArray.forEach(element => {
          // ...use `element`...
      });
      
    • 一个简单的老式循环 - 友好forasync
      for (let index = 0; index < theArray.length; ++index) {
          const element = theArray[index];
          // ...use `element`...
      }
      
    • (很少)有保障措施 - 友好for-inasync
      for (const propertyName in theArray) {
          if (/*...is an array element property (see below)...*/) {
              const element = theArray[propertyName];
              // ...use `element`...
          }
      }
      
  • 一些快速的“不要”:

    • 不要使用for-in,除非你在保护措施下使用它,或者至少知道为什么它可能会咬你。
    • 如果您不使用 map 的返回值,请不要使用 map
      (可悲的是,有人在那里教地图[spec / MDN],好像它是forEach - 但正如我在博客上写的那样,这不是它的目的。如果未使用它创建的数组,请不要使用 map
    • 如果回调执行异步工作,并且您希望等到该工作完成(因为它不会),则不要使用 forEachforEach

但还有很多东西需要探索,请继续阅读...


JavaScript具有强大的语义,用于循环通过数组和类似数组的对象。我将答案分为两部分:正版数组的选项,以及类似数组的事物的选项,例如对象,其他可迭代对象(ES2015 +),DOM集合等。arguments

好吧,让我们看看我们的选项:

对于实际数组

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

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

(你可以在这里看到那些旧的规范:ES5ES2015,但两者都已被取代;当前编辑的草稿总是在这里

详:

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

ES2015 在 JavaScript 中添加了迭代器和可迭代对象。数组是可迭代的(字符串、s 和 s 以及 DOM 集合和列表也是如此,稍后您将看到)。可迭代对象为其值提供迭代器。new 语句循环访问迭代器返回的值:MapSetfor-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

没有比这更简单的了!在掩护下,它从数组中获取迭代器,并循环访问迭代器返回的值。数组提供的迭代器按从头到尾的顺序提供数组元素的值。

请注意如何将范围限定为每个循环迭代;尝试在循环结束后使用将失败,因为它不存在于循环主体之外。elementelement

从理论上讲,循环涉及多个函数调用(一个用于获取迭代器,另一个用于从中获取每个值)。即使这是真的,也没什么可担心的,函数调用在现代JavaScript引擎中非常便宜(在我研究它之前,它困扰着我[下面];详细信息)。但除此之外,JavaScript引擎在处理数组等事物的本机迭代器时优化了这些调用(在性能关键代码中)。for-offorEach

for-of完全友好。如果您需要循环体中的工作以串联方式(而不是并行)完成,则循环体中的工作将等待承诺稳定下来,然后再继续。这是一个愚蠢的例子:asyncawait

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添加的功能,您可以使用(规范|MDN)如果您只处理同步代码(或者您不需要在循环期间等待异步进程完成):ArrayforEach

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

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

像 ,具有您不必在包含范围中声明索引和值变量的优点;在本例中,它们作为参数提供给迭代函数,并且很好地限定为该迭代的范围。for-offorEach

与 不同,它的缺点是它不理解函数和 。如果使用函数作为回调,请不要等待该函数的承诺结算后再继续。下面是使用代替的示例 - 请注意初始延迟是如何存在的,但随后所有文本都会立即显示,而不是等待:for-offorEachasyncawaitasyncforEachasyncfor-offorEach

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 (规格|MDN)- 在回调首次返回假值时停止循环
  • some (规格|MDN)- 在回调首次返回真值时停止循环
  • filter (规格|MDN)- 创建一个新数组,其中包含回调返回真实值的元素,省略不返回真实值的元素
  • map (规格|MDN)- 从回调返回的值创建新数组
  • reduce (规格|MDN)- 通过重复调用回调来建立一个值,传入以前的值;有关详细信息,请参阅规范
  • reduceRight (规格|MDN)- 像 ,但按降序工作,而不是升序reduce

与 一样,如果使用函数作为回调,则不会等待函数的承诺稳定下来。这意味着:forEachasync

  • 使用函数回调永远不适合 , ,并且因为它们会将返回的 promise 视为真实值;他们不会等待承诺解决,然后使用履行价值。asynceverysomefilter
  • 如果目标是将某物数组转换为承诺数组,则通常适合使用 函数回调,也许用于传递给其中一个承诺组合函数(Promise.all、Promise.racepromise.allSettledPromise.any)。asyncmap
  • 使用函数回调很少适合与 或 一起使用,因为(再次)回调将始终返回一个 promise。但是有一个成语是从使用()的数组构建一个承诺链,但通常在这些情况下,函数中的or循环会更清晰,更容易调试。asyncreducereduceRightreduceconst promise = array.reduce((p, element) => p.then(/*...something using `element`...*/));for-offorasync

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之前,循环变量必须存在于包含范围中,因为只有函数级范围,而不是块级范围。但是,正如您在上面的示例中看到的那样,您可以在 将变量的作用域限定为循环。当你这样做时,变量会为每个循环迭代重新创建,这意味着在循环体中创建的闭包会保留对该特定迭代的引用,这解决了旧的“循环中的闭包”问题:varletforindexindex

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

在上面,如果您单击第一个,则得到“索引为:0”,如果单击最后一个,则得到“索引为:4”。如果您使用而不是(您始终会看到“索引为:5”),则此操作不起作用varlet

就像 ,循环在函数中运行良好。下面是使用循环的早期示例:for-offorasyncfor

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. 该对象具有该名称自己的属性(不是从其原型继承的属性;此检查也经常编写为但ES2022添加了Object.hasOwn,这可能更可靠),以及a.hasOwnProperty(name)

  2. 名称都是十进制数字(例如,普通字符串形式,而不是科学记数法),以及

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

...尽管话虽如此,但大多数代码只进行检查。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);
});

如 ,如果异步函数中的工作需要串联完成,则它工作得很好。forfor-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);
}

迭代器是与规范中的迭代器定义匹配的对象。它的方法在每次调用它时都会返回一个新的结果对象。result 对象有一个属性 ,告诉我们它是否已完成,以及一个具有该迭代值的属性。( 如果是可选的,则为可选,如果是可选的,则为可选。nextdonevaluedonefalsevalueundefined

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

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

由于迭代器对象在你调用之前不会前进,它们在函数循环中工作得很好。下面是显式使用迭代器的早期示例:nextasyncfor-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

对于类似数组的对象

除了真正的数组之外,还有一些类似数组的对象具有属性和具有全数字名称的属性:NodeList实例HTMLCollection实例,对象等。我们如何循环浏览它们的内容?lengtharguments

使用上述大多数选项

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

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

    for-of使用对象提供的迭代器(如果有)。这包括主机提供的对象(如 DOM 集合和列表)。例如,来自方法的实例和来自两者的 s 实例都支持迭代。(这是由HTML和DOM规范非常微妙地定义的。基本上,任何具有索引访问权限和索引访问权限的对象都是自动可迭代的。它不必被标记为;仅用于集合,除了可迭代之外,还支持 、 、 和方法。 确实如此; 没有,但两者都是可迭代的。HTMLCollectiongetElementsByXYZNodeListquerySelectorAlllengthiterableforEachvalueskeysentriesNodeListHTMLCollection

    下面是循环访问元素的示例: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+)

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

    假设你想在 的集合上使用(作为 一个 ,本身没有)。您将执行以下操作:forEachNodechildNodesHTMLCollectionforEach

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

    (但请注意,您可以只在 上使用。for-ofnode.childNodes

    如果你要这样做很多,你可能想把函数引用的副本抓到一个变量中以供重用,例如:

    // (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. 使用简单的 for 循环

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

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

    请参阅 #1。

您可能可以逃脱(通过保护措施),但是有了所有这些更合适的选择,就没有理由尝试。for-in

创建真正的数组

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

  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 from and makes an array from it.NodeListquerySelectorAll

    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 , this uses the iterator provided by the object (see #1 in the previous section):for-of

    const trueArray = [...iterableObject];
    

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

    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 into a true array, we could do this:NodeList

    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 like that.)this

Caveat for host-provided objects

If you use 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.)Array.prototype


答案 2

Note: This answer is hopelessly out-of-date. For a more modern approach, look at the methods available on an array. Methods of interest might be:

  • forEach
  • map
  • filter
  • zip
  • reduce
  • every
  • some

The standard way to iterate an array in JavaScript is a vanilla -loop:for

var length = arr.length,
    element = null;
for (var i = 0; i < length; i++) {
  element = arr[i];
  // Do something with element
}

Note, however, that this approach is only good if you have a dense array, and each index is occupied by an element. If the array is sparse, then you can run into performance problems with this approach, since you will iterate over a lot of indices that do not really exist in the array. In this case, a -loop might be a better idea. However, you must use the appropriate safeguards to ensure that only the desired properties of the array (that is, the array elements) are acted upon, since the -loop will also be enumerated in legacy browsers, or if the additional properties are defined as .for .. infor..inenumerable

In ECMAScript 5 there will be a forEach method on the array prototype, but it is not supported in legacy browsers. So to be able to use it consistently you must either have an environment that supports it (for example, Node.js for server side JavaScript), or use a "Polyfill". The Polyfill for this functionality is, however, trivial and since it makes the code easier to read, it is a good polyfill to include.