Array.prototype.forEach()

​forEach()​​ 方法对数组的每个元素执行一次给定的函数。

语法

​arr.forEach(callback(currentValue [, index [, array]])[, thisArg])​

  • ​callback​​:为数组中每个元素执行的函数,该函数接收一至三个参数:
  • ​currentValue​​:数组中正在处理的当前元素。
  • ​index​​: 可选,数组中正在处理的当前元素的索引。
  • ​array​​​: 可选,​​forEach()​​ 方法正在操作的数组。

​thisArg​​​: 可选,当执行回调函数 ​​callback​​​ 时,用作 ​​this​​ 的值。

返回值:​​undefined​​。

  • ​forEach()​​​ 为每个数组元素执行一次​​callback​​​ 函数;与​​map()​​​ 或者​​reduce()​​​ 不同的是,它总是返回​​undefined​​ 值,并且不可链式调用。
  • ​forEach()​​​ 被调用时,不会改变原数组(尽管​​callback​​ 函数在被调用时可能会改变原数组)
  • 由于​​forEach​​​ 的返回值是​​undefined​​,所以 forEach 无法进行链式调用

使用示例

不对未初始化的值进行任何操作

如下稀疏数组,​​3​​​ 和 ​​7​​​ 之间空缺的数组单元未被 ​​forEach()​​​ 调用 ​​callback​​ 函数,或进行任何其他操作

const arraySparse = [1, 3, , 7];
let numCallbackRuns = 0;

arraySparse.forEach(function (element) {
console.log(element);
numCallbackRuns++;
});

console.log("numCallbackRuns: ", numCallbackRuns);


如果数组在迭代时被修改了,则其他元素会被跳过

当到达包含值 ​​two​​​ 的项时,整个数组的第一个项被移除了,这导致所有剩下的项上移一个位置。因为元素 ​​four​​​ 正位于在数组更前的位置,所以 ​​three​​ 会被跳过。

var words = ['one', 'two', 'three', 'four'];
words.forEach(function (word) {
console.log(word);
if (word === 'two') {
words.shift();
}
});


中断循环

在 ​​forEach​​​ 中中断循环可以使用 ​​return​​​,因为 ​​forEach​​​ 中的 ​​callback​​​ 是一个回调函数,在函数中可以使用 ​​return​​ 来中断循环

const arr = [1, 3, 66, 88, 999]
arr.forEach((item) => {
if (item === 66) {
return
}
console.log(item);
})


由于 ​​break​​​ 与 ​​continue​​​ 必须在一个循环体内使用,所以不能直接在 ​​forEach​​ 里使用,不能直接用它们来中断循环

forEach 对于 async 的处理问题

先看一段正常的代码

let sum = 0
const arr = [1, 2, 3]

function sumFn (a,) {
return a + b
}

function main (array) {
array.forEach(item => {
sum = sumFn(sum, item)
console.log(sum);
})

console.log(sum)
}

main(arr)

输出结果如下


将上述代码改造为 ​​promise​​ 的形式

let sum = 0
const arr = [1, 2, 3]

function sumFn (a,) {
return new Promise((resolve,) => {
setTimeout(() => {
resolve(a + b)
}, 1000 * b)
})
}

async function main (array) {
array.forEach(async item => {
sum = await sumFn(sum, item)
console.log(sum);
})

console.log(sum)
}
main(arr)


为什么打印的结果和上述的不一样呢,分析如下

当代码执行到 forEach 时:

  1. 首先遇到​​sum = await sumFn(sum, item)​​ 语句(注意,它是从右往左执行的)
    因此,它会执行 ​​sumFn(0, 1)​​,那么该函数 ​​return 1​​,
    由于 ​​async​​ 函数始终会返回一个 ​​Promise​​ 对象,即 ​​return Promise.resolve(1)​​。
  2. 由于​​await​​ 的原因,它其实相当于执行 ​​Promise.resolve(1).then()​​ 方法,
    它属于微任务,会暂时 ​​Hold​​ 住,被放入微任务的队列,待本次同步任务执行完之后,
    才会被执行,因此并不会立即赋值给 ​​sum​​(所以 ​​sum​​ 仍为 ​​0​​)。
  3. 那​​JS​​ 引擎主线程不会闲着的,它会继续执行“同步任务”,即下一次循环。
    同理,又将 ​​return Promise.resolve(2)​​ 放入微任务队列。
    直到最后一次循环,同样的的还是 ​​return Promise.resolve(3)​​。
    其实到这里,​​forEach​​ 其实算是执行完了。
    以上示例,​​forEach​​ 的真正意义是创建了 ​​3​​ 个微任务。
  4. 由于主线程会一直执行同步任务,待同步任务执行完之后,才会执行任务队列里面的微任务。
    待 ​​forEach​​ 循环结束之后,自然会执行 ​​console.log(sum)​​,
    但注意,由于 ​​await​​ 的原因,​​sum​​ 一直未被重新赋值,因此 ​​sum​​ 还是为 ​​0​​ ,
    所以控制台输出了 ​​0​​。
  5. 等​​console.log(sum)​​ 执行完毕,才开始执行队列中的微任务,
    其中 ​​await Promise.resolve(0)​​ 的结果,
    相当于 ​​Promise.resolve(0)​​ 的 then 方法的返回值,
    所以此前的三个微任务,相当于:
    ​sum = 1​​​​sum = 2​​​​sum = 3​​ 它们被依次执行。
  6. 所以输出的结果与预期是有区别的

如何解决上述问题

使用 ​​for...of​​​ 循环去处理 ​​async await​​​ 的问题,​​for...of​​​ 本质上就是一个 ​​while​​ 循环。

let sum = 0
const arr = [1, 2, 3]

function sumFn (a,) {
return new Promise((resolve,) => {
setTimeout(() => {
resolve(a + b)
}, 1000 * b)
})
}

async function main (array) {
for (const item of array) {
sum = await sumFn(sum, item)
console.log(sum);
}

// 相当于
// const iterator = array[Symbol.iterator]()
// let iteratorResult = iterator.next()
// while (!iteratorResult.done) {
// sum = await sumFn(sum, iteratorResult.value)
// iteratorResult = iterator.next()
// }

console.log(sum)
}
main(arr)