在JavaScript中遍历数组(for each循环)

5654

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


使用 for...of 循环。请参阅 https://www.w3schools.com/JS/js_loop_forof.asp。 - user19690494
与“如何在JavaScript中循环遍历数组”几乎相同,但略微更为通用的内容。 - outis
41个回答

8361

简而言之

  • 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).

但探索的内容还有很多,继续阅读...


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 中。数组是可迭代的(字符串、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完全支持async。如果需要在循环体中完成的工作是按顺序(而不是并行)完成的,则循环体中的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 (规范 | MDN)。

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

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

for-of类似,forEach的优点是您不必在包含范围中声明索引和值变量;在这种情况下,它们作为迭代函数的参数提供,因此仅限于该迭代的范围内。

for-of不同,forEach的缺点在于它不理解async函数和await。如果您将async函数用作回调,则forEach在继续之前不会等待该函数的承诺解决。这是使用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` omitted because we know it never rejects

forEach是一个循环遍历数组的函数,但是ES5定义了其他几个有用的“通过数组并执行操作”函数,包括:

  • every (规范 | MDN) - 当回调返回假值时停止循环
  • some (规范 | MDN) - 当回调返回真值时停止循环
  • filter (规范 | MDN) - 创建一个新数组,其中包括回调返回真值的元素,省略不返回真值的元素
  • map (规范 | MDN) - 根据回调返回的值创建一个新数组
  • reduce (规范 | MDN) - 通过重复调用回调并传递先前的值来构建一个值;详细信息请参见规范
  • reduceRight (规范 | MDN) - 类似于reduce,但是按降序而不是升序工作

forEach一样,如果您将异步函数用作回调,则这些函数都不会等待函数的承诺解决。这意味着:

  • 不能使用async函数回调来处理everysomefilter, 因为它们会将返回的promise视为真值;它们不会等待promise被解决并使用兑现的值。
  • 如果目标是将某个数组转换成一个promises数组,以便传递给其中一个promise组合器函数(Promise.allPromise.racepromise.allSettledPromise.any),则通常需要使用mapasync函数回调。
  • 很少情况下,可以使用async函数回调来处理reducereduceRight, 因为(再一次)回调函数总是返回一个promise。但一种从数组构建promise链的惯用语法使用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>

在上面的例子中,如果你点击第一个,你会得到“索引是:0”,如果你点击最后一个,你会得到“索引是:4”。如果你使用var而不是let,这个方法就不会起作用(你总是会看到“索引是:5”)。
for-of一样,在async函数中,for循环也能很好地工作。下面是之前的例子,使用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,而2^32 - 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方法在每次调用时返回一个新的结果对象。结果对象具有一个属性done告诉我们它是否完成,还有一个可选的属性value表示该迭代的值(如果这个属性为false,那么done是可选的;如果这个属性为undefined,那么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` omitted because we know it never rejects

针对类数组对象

除了真正的数组,还有一些具有类数组特性的对象,它们具有length属性和以数字为名称的属性,例如NodeList实例HTMLCollection实例arguments对象等。如何遍历它们的内容呢?

使用上面大部分选项

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

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

    for-of 使用对象提供的迭代器(如果有)。这包括由主机提供的对象(如 DOM 集合和列表)。例如,从 getElementsByXYZ 方法返回的HTMLCollection实例和从 querySelectorAll 返回的NodeList实例都支持迭代。(这在 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. 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.

你也许可以使用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 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.)

主机提供的对象的注意事项

如果您使用Array.prototype函数与主机提供的类似数组的对象(例如,由浏览器提供而不是JavaScript引擎提供的DOM集合等),过时的浏览器(如IE8)可能无法正确处理,所以如果您需要支持这些浏览器,请务必在目标环境中进行测试。但对于现代浏览器来说,这不是问题。(对于非浏览器环境,自然会取决于环境。)


3
@Alex - 数组上不代表数组元素的属性。例如:const a = ["a", "b"]; a.example = 42; 这个数组有三个属性(除了所有数组都有的属性),它们的名称是字符串 "0""1""example"。名为 "example" 的属性是非元素属性。其他两个是元素属性,因为它们表示数组的元素。 - T.J. Crowder
1
@PeterKionga-Kamau - 这不是一个关联数组,而是一个对象。你在 var arr = new Array(); 中创建的数组被抛弃并被你在 arr = {"test":"testval", "test2":"test2val"}; 中创建的对象所取代。这段代码应该只写成 var arr = {"test":"testval", "test2":"test2val"}; (好吧,不是 var 而是 letconst)。尽管按照某些定义,对象可能被认为是关联数组,但按照其他定义,它们不是,并且我在 JavaScript 中避免使用这个术语,因为它在 PHP 中有特定的含义,PHP 与 JavaScript 都在 Web 工作中广泛使用。 - T.J. Crowder
1
@PeterKionga-Kamau - 这个问题和回答都是关于数组,而不是其他对象。但是:对象属性没有索引,所以对象没有索引访问;相反,它们具有键控访问(theObject.propNametheObject["propName"]theObject[propKeySymbol]等)。对于对象来说,索引访问的概念并不有用。虽然非常间接地实现是可能的。对象属性现在有一个顺序(ES2015+,在几个后续规范中进行了一些调整),但这个顺序很复杂,它取决于属性创建的顺序、属性键的类型,... - T.J. Crowder
1
如果属性键是字符串,则返回属性键的值(!),以及属性是继承的还是“自有”的,因此依赖于属性顺序是不好的实践。如果仍然想要这样做,没有一种操作可以按顺序提供所有属性键,但是Reflect.ownKeys按顺序提供对象自有属性键的数组(跳过继承的属性)。因此,如果适用于用例,则可以从中获取数组(const keys = Reflect.ownKeys(theObject);)。... - T.J. Crowder
2
let keys = []; for (let obj = theObject; obj; obj = Object.getPrototypeOf(obj)) { keys.push(...Reflect.ownKeys(obj)); } keys = [...new Set(keys)]; 这段代码的作用是,获取对象及其原型链上所有可枚举的属性名,并去重后返回一个数组。这是因为在 JavaScript 中,如果一个对象拥有多个原型,那么它的属性查找顺序是先查找自己的属性,再查找第一个原型的属性,然后是第一个原型的原型的属性,以此类推。因此我们需要按照这个顺序逐层遍历对象和其原型,将所有可枚举的属性名存储在一个数组中,最后使用 Set 去重并返回结果数组。如果一个属性同时存在于对象和它的原型中,则只保留对象自身的属性名。 - T.J. Crowder
显示剩余14条评论

569

注意:此答案已经过时。要了解更现代的方法,请查看数组可用的方法。感兴趣的方法可能包括:

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

有没有一种方法可以只用一行代码来完成呢?例如在 Facebook 上,我想要加速视频,使用 document.getElementsByTagName("video")[28].playbackRate = 2.2。如果我能够轻松地遍历所有元素,那么我就可以避免识别哪个视频(例如在这种情况下是索引28)。有什么想法吗? - stevec
4
@stevec: 使用Array.from(document.querySelectorAll('video')).forEach(video => video.playbackRate = 2.2);可以将所有视频的播放速度调整为2.2倍。 - PatrikAkerstrand
你提供的关于数组可用方法的链接(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)并没有提供`.zip`的任何信息 - 它是如何工作的?类似于Python吗? - Cadoiz

275

如果你正在使用 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].
}

150

倒序循环

我认为这里应该提到反向循环:

for (var i = array.length; i--; ) {
     // process array[i]
}

优点:

  • 你不需要声明一个临时的len变量,也不需要在每次迭代中与array.length进行比较,这两种方法都可能是微小的优化。
  • 从DOM中删除兄弟节点通常更加高效。(浏览器需要在其内部数组中移动元素的次数更少。)
  • 如果你在循环过程中修改了数组,在索引i之后或之前(例如,你删除或插入了一个项目array[i]),那么正向循环将跳过向左移动到位置i的项,或重新处理向右移动的第i个项。在传统的for循环中,你可以更新i以指向下一个需要处理的项-1,但简单地反转迭代方向通常是一个更加简单和更优雅的解决方案
  • 同样,在修改或删除嵌套的DOM元素时,倒序处理可以规避错误。例如,考虑在处理子元素之前修改父节点的innerHTML。当子节点被访问时,它将从DOM中分离出来,因为它已经被新创建的子节点替换了,而这个新节点是在父节点的innerHTML被写入时创建的。
  • 它比其他可用选项更加简洁易读,虽然不如forEach()和ES6的for ... of

缺点:

  • 它按相反的顺序处理项目。如果您正在从结果构建新数组,或在屏幕上打印内容,则输出将与原始顺序相反
  • 重复将同级节点作为第一个子节点插入DOM以保留其顺序效率较低。(浏览器将不断向右移动东西。)要高效地按顺序创建DOM节点,请正常循环并追加(还要使用“文档片段”)。
  • 反向循环对初级开发人员来说很困惑。(根据您的观点,您可能认为这是优点。)

我应该总是使用它吗?

一些开发人员默认使用反向循环,除非有充分的理由需要正向循环。

虽然性能增益通常微不足道,但这似乎在喊:

"对列表中的每个项目都这样做,我不在乎顺序!"

但是,在实践中,这实际上并不能可靠地表明意图,因为它与那些你在意顺序的场合以及确实需要反向循环的场合无法区别。因此,实际上需要另一个结构来准确地表示"不在乎"的意图,这在大多数语言,包括ECMAScript中当前不可用,但可以称之为forEachUnordered()

如果顺序不重要,并且效率是一个问题(例如在游戏或动画引擎的最内层循环中),那么使用反向循环作为您的首选模式可能是可以接受的。只需记住,在现有代码中看到反向for循环并不一定意味着顺序不重要!

最好使用forEach()。

一般来说,对于更高级的代码,其中清晰性和安全性更受关注,我之前建议使用Array::forEach作为循环的默认模式(尽管现在我更喜欢使用for..of)。 选择forEach而不是反向循环的原因如下:
  • 它更容易阅读。
  • 它表明i不会在块内移动(这总是可能隐藏在长forwhile循环中的惊喜)。
  • 它为闭包提供了免费的范围。
  • 它减少了本地变量的泄漏和与外部变量的意外冲突(以及变异)。

然后,当您在代码中看到反向循环时,这表明它被反转有一个很好的理由(也许是上述原因之一)。而看到传统的正向循环可能表明移位可以发生。

如果“意图”的讨论对您来说毫无意义,那么您和您的代码可能会受益于观看Crockford关于编程风格和大脑的演讲。

现在使用for..of更好了!

关于for..offorEach()哪个更好的争论还在继续:

  • 为了获得最大的浏览器支持,for..of 需要一个迭代器的polyfill,这会使你的应用程序执行稍微变慢,下载稍微变大。

  • 因此(并且为了鼓励使用mapfilter),一些前端样式指南完全禁止使用for..of

  • 但是以上问题不适用于Node.js应用程序,在那里for..of现在得到了很好的支持。

  • 此外,forEach()内部无法使用await。在这种情况下,使用for..of是最清晰的模式

个人而言,我倾向于使用看起来最容易阅读的内容,除非性能或缩小文件大小是一个重大问题。因此,现在我更喜欢使用for..of而不是forEach(),但在适用时我仍然会使用mapfilterfindsome。(出于同事的考虑,我很少使用reduce.)


它是如何工作的?

for (var i = 0; i < array.length; i++) { ... }   // Forwards

for (var i = array.length; i--; )    { ... }   // Reverse

你会注意到i--是中间子句(通常我们在那里看到比较),而最后一个子句是空的(通常我们在那里看到i++)。这意味着i--也被用作继续的条件。关键是,在每次迭代之前执行和检查它。
  • 如何在不溢出的情况下从array.length开始?

    因为i--在每次迭代之前运行,所以在第一次迭代中,我们实际上将访问array.length-1处的项目,这避免了任何与数组越界 undefined项相关的问题。

  • 为什么它不会在索引0之前停止迭代?

    当条件i--计算出一个falsy值(当它产生0时)时,循环将停止迭代。

    诀窍在于,与--i不同,尾随的i--操作符会递减i但产生在递减之前的值。在控制台上可以演示这一点:

    > var i = 5; [i, i--, i];

    [5, 5, 4]

    因此,在最后一次迭代中,i先前为1,而i--表达式将其更改为0,但实际上产生了1(真),因此条件通过。在下一次迭代中,i--i更改为-1,但产生了0(假),导致执行立即退出循环底部。

    在传统的正向for循环中,i++++i是可互换的(如Douglas Crockford所指出的)。然而,在反向for循环中,因为我们的递减也是我们的条件表达式,如果我们想处理索引0处的项目,我们必须坚持使用i--


小知识

有些人喜欢在反向for循环中绘制一个小箭头,并以一个眨眼结束:

for (var i = array.length; i --> 0 ;) {

感谢WYL向我展示了反向循环的好处和可怕之处。


99

一些 C 风格的语言使用 foreach 来循环枚举。在 JavaScript 中,可以使用 for..in 循环结构实现:

var index,
    value;
for (index in obj) {
    value = obj[index];
}

有个需要注意的地方。 for..in 循环会遍历对象本身和原型链上所有可枚举的属性。为了避免读取从对象原型中继承而来的属性值,只需检查属性是否属于对象即可:

for (i in obj) {
    if (obj.hasOwnProperty(i)) {
        //do stuff
    }
}

另外,ECMAScript 5 添加了一个forEach方法到Array.prototype,可以使用回调函数来枚举数组(文档中提供了 polyfill,因此您仍然可以使用它来支持旧的浏览器):

arr.forEach(function (val, index, theArray) {
    //do stuff
});

需要注意的是,Array.prototype.forEach在回调函数返回false时并不会停止循环。 jQueryUnderscore.js提供了它们自己的each变体,可以提供可短路的循环。


83

for...of | forEach | map

使用现代JavaScript语法迭代数组

const fruits = ['', '', '' ]

  for...of

的翻译如下:

  for...of

for (const fruit of fruits) {
    console.log(fruit)  // '', '', ''
}

forEach

fruits.forEach(fruit => {
    console.log(fruit)  // '', '', ''
})

映射函数

*与前两者不同,map() 创建一个新的数组并期望在每次迭代后您返回一些内容。

fruits.map(fruit => fruit)   // ['', '', '' ]

重要提示: map() 方法旨在在每次迭代中返回一个值,它是转换数组元素的理想方法:

fruits.map(fruit => 'cool ' + fruit)   // ['cool ', 'cool ', 'cool ' ]

另一方面,for...offorEach( )不需要返回任何值,因此我们通常使用它们执行逻辑任务来操纵外部的操作。

也就是说,在这两个函数中,你会发现 if() 语句、副作用和日志记录活动。

提示:在 .map() 或 .forEach() 中,你还可以在每次迭代中获取索引(以及整个数组)。只需向它们传递附加参数即可:

fruits.map((fruit, i) =>  i + '  ' + fruit)

// ['0 ', '1 ', '2 ' ]

fruits.forEach((f, i, arr) => {
    console.log( f + ' ' + i + ' ' +  arr )
})

//   0  , , ,
//   1  , , ,
//   2  , , ,

55

如果你想循环遍历一个数组,可以使用标准的三部分 for 循环。

for (var i = 0; i < myArray.length; i++) {
    var arrayItem = myArray[i];
}

通过缓存myArray.length或者反向迭代可以获得一些性能优化。


43

如果您不介意清空数组:

var x;

while(x = y.pop()){ 

    alert(x); //do something 

}

x 将包含 y 的最后一个值,并且该值将从数组中被移除。你也可以使用 shift() 方法从 y 中获取并移除第一个元素。


40
我知道这是一个旧帖子,已经有很多很好的答案了。为了更加完整,我想使用AngularJS提供另一种答案。当然,这仅适用于使用Angular的情况,尽管如此,我还是想把它放在这里。 angular.forEach需要两个参数和一个可选的第三个参数。第一个参数是要迭代的对象(数组),第二个参数是迭代器函数,可选的第三个参数是对象上下文(在循环内部称为“this”)。
有不同的方法来使用angular的forEach循环。最简单且可能最常用的方法是:
var temp = [1, 2, 3];
angular.forEach(temp, function(item) {
    //item will be each element in the array
    //do something
});

另一种将项目从一个数组复制到另一个数组的有用方法是:
var temp = [1, 2, 3];
var temp2 = [];
angular.forEach(temp, function(item) {
    this.push(item); //"this" refers to the array passed into the optional third parameter so, in this case, temp2.
}, temp2);

虽然如此,你不必这样做,你可以简单地执行以下操作,它等同于先前的示例:

angular.forEach(temp, function(item) {
    temp2.push(item);
});

现在使用angular.forEach函数和内置的原生for循环有优缺点。 优点:
  • 易读性高
  • 易写性高
  • 如果可用,angular.forEach将使用ES5 forEach循环。现在,在缺点部分,我们将讨论forEach循环比for循环慢得多的效率问题。我之所以将这个作为优点提到,是因为保持一致和标准化很好。

考虑以下两个嵌套循环,它们完全相同。假设我们有两个对象数组,每个对象包含一个结果数组,每个结果都有一个值属性(或其他)。假设我们需要迭代每个结果,如果它们相等,则执行某些操作:

angular.forEach(obj1.results, function(result1) {
    angular.forEach(obj2.results, function(result2) {
        if (result1.Value === result2.Value) {
            //do something
        }
    });
});

//exact same with a for loop
for (var i = 0; i < obj1.results.length; i++) {
    for (var j = 0; j < obj2.results.length; j++) {
        if (obj1.results[i].Value === obj2.results[j].Value) {
            //do something
        }
    }
}

虽然这只是一个非常简单的假设性例子,但我已经使用第二种方法编写了三重嵌套的for循环,阅读和编写都非常困难。

缺点

  • 效率。无论是angular.forEach还是原生的forEach,都比普通的for循环慢得多...大约90%的速度。因此对于大型数据集,最好坚持使用原生的for循环。
  • 不支持break、continue或return。 "accident"实际上支持continue,要在angular.forEach中继续,只需在函数中放置一个return;语句,如angular.forEach(array, function(item) { if (someConditionIsTrue) return; });,这将导致它在该迭代中退出函数。这也是因为原生的forEach也不支持break或continue。

我相信还有其他的优缺点,如果您认为有必要,请随意添加。总之,如果您需要效率,请坚持使用本地的for循环来满足您的循环需求。但是,如果您的数据集较小,并且可以牺牲一些效率以换取可读性和可写性,那么请在其中添加一个angular.forEach


40

一个 forEach 实现(在 jsFiddle 中查看):

function forEach(list,callback) {
  var length = list.length;
  for (var n = 0; n < length; n++) {
    callback.call(list[n]);
  }
}

var myArray = ['hello','world'];

forEach(
  myArray,
  function(){
    alert(this); // do something
  }
);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接