在 JavaScript 中的数组上的 For-each

如何解决在 JavaScript 中的数组上的 For-each?

开发过程中遇到在 JavaScript 中的数组上的 For-each的问题如何解决?下面主要结合日常开发的经验,给出你关于在 JavaScript 中的数组上的 For-each的解决方法建议,希望对你解决在 JavaScript 中的数组上的 For-each有所启发或帮助;

问题描述

  • 的选择通常是

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

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

  • forEach(ES5 +只;规格| MDN)(或其亲属some和这样) - async-友好(而见详情)

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

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

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

    (很少) for-in 有保护措施- -async友好

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

  • 一些快速的“不要”:

  • 除非您使用它有安全措施,或者至少知道它为什么会咬你。

  • (可悲的是有人在那里教map[ spec / MDN ] 好像是这样forEach ——但正如我在我的博客上写的那样,这不是它的用途。如果你不使用它创建的数组,请不要使用map。)
  • ,如果回调不异步工作,并且希望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+)

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

细节:

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

ES2015向 JavaScript添加迭代器和可迭代对象。数组是可迭代的(字符串、Maps 和Sets 以及 DOM 集合和列表也是如此,稍后您将看到)。可迭代对象为其值提供迭代器。newfor-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[below] 直到我研究它;细节)。但此外,在处理数组等本机迭代器时,JavaScript 引擎会优化这些调用(在性能关键代码中)。

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

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及相关

在任何可以访问ArrayES5 添加功能的模糊现代环境(因此,不是 IE8)中,如果您只处理同步代码(或者您不需要等待),则可以使用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则在继续之前等待该函数的承诺解决。这是使用替代的async示例- 请注意初始延迟是如何出现的,但随后所有文本都会立即出现而不是等待:for-of``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( spec | MDN ) - 类似reduce,但按降序而不是升序工作

和 一样forEach,如果你使用一个async函数作为你的回调函数,那么这些函数都不会等待函数的承诺完成。这意味着:

  • 使用一个async回调函数从来没有通过适当的everysome以及filter因为它们将把返回的承诺,就好像是一个truthy值; 他们等待承诺解决,然后使用履行价值。
  • 使用async函数回调通常适用于map,目标是将某个数组转换为promise数组,可能是为了传递给一个 promise 组合器函数Promise.allPromise.racepromise.allSettled、 或Promise.any)。
  • 使用async函数回调很少适合与reduceor reduceRight,因为(再次)回调将始终返回一个承诺。但是有一种习惯用法是从使用reduce( const promise = array.reduce((p, element) => p.then(/*...something usingelement...*/));)的数组构建承诺链,但通常在这些情况下,函数中的for-oforfor循环async会更清晰且更易于调试。

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

And/or 倒数:

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

但是对于现代 JavaScript 引擎,您很少需要勉强挤出最后一点力气。

在 ES2015 之前,循环变量必须存在于包含作用域中,因为var只有函数级作用域,没有块级作用域。但是正如您在上面的示例中看到的那样,您可以let在 内使用for将变量范围限定为循环。当你这样做时,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(你永远看“:5指数”)。

就像for-offor循环在async函数中运行良好。这是使用for循环的较早示例:

Show code snippet

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^32 - ,这是数组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

显式使用迭代器(ES2015+)

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

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

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

你得到的东西value取决于迭代器。阵列上,缺省迭代器提供每个阵列元素的值("a""b",和"c"在前面的例子)。数组还有其他三个返回迭代器的方法

  • values():这是[Symbol.iterator]返回认迭代器的方法的别名。
  • keys(): 返回一个迭代器,它提供数组中的每个键(索引)。在上面的示例中,它将提供"0", then "1", then "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属性和全数字名称属性NodeListinstancesHTMLCollectioninstancesargumentsobject 等。我们如何遍历它们的内容

使用上面的大部分选项

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

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

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

Show code snippet

on 的各种函数Array.prototype是“有意通用的”,可以通过Function#call( @L_403_36@ | MDN ) 或Function#apply( spec | MDN )用于类数组对象。(如果您必须处理 IE8 或更早版本 [哎哟],请参阅本答案末尾的“主机提供对象的警告”,但这对于模糊现代的浏览器来说不是问题。)

假设您想forEach在 aNodechildNodes集合上使用(作为 ,本机HTMLCollection没有forEach)。你会这样做:

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

(但请注意,您可以只使用for-ofon node.childNodes。)

如果您打算经常这样做,您可能希望将函数引用的副本抓取到变量中以供重用,例如:

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

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

见#1。

您能够逃脱for-in(使用安全措施),但是有了所有这些更合适的选项,就没有理由尝试了。

创建一个真正的数组

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

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

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

获取NodeListfromquerySelectorAll并从中创建一个数组。

如果您打算以某种方式映射内容,映射功能会很方便。例如,如果您想获取具有给定类的元素的标签名称数组:

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

也可以使用 ES2015 的扩展语法。就像for-of,这使用了对象提供的迭代器(参见上一节中的 #1):

js const trueArray = [...iterableObject];

因此,例如,如果我们想将 a 转换NodeList一个真正的数组,使用展开语法这会变得非常简洁:

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

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

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

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

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

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

主机提供的对象的警告

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

解决方法

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

我以为是这样的:

forEach(instance in theArray)

theArray我的数组在哪里,但这似乎不正确。

喜欢与人分享编程技术与工作经验,欢迎加入编程之家官方交流群!

猜你在找的编程问答相关文章

怎样才能让 Git“忘记”一个被跟踪但现在在 .gitignore 中的文件?
Python 中的metaclasses是什么?
如何在 Linux 上查找包含特定文本的所有文件?
如何从异步调用返回响应
如何在一个表达式中合并两个字典(取字典的并集)?
HTTP 中的 POST 和 PUT 有什么区别?
使用 Git 将最近的提交移动到新分支
在 JavaScript 比较中应该使用哪个等于运算符 (== vs ===)?
微信公众号搜索 “ 程序精选 ” ,选择关注!
微信公众号搜 "程序精选"关注