关于 JavaScript 中的 forEach 循环你不知道的 8 件事
熟悉 PHP 的开发者,第一次看到使用 .forEach()
方法来遍历数组时,大多数认为这与标准 for
循环的实现完全相同。在深入学习 JavaScript 之后,很快就能意识到两者之间存在差异。本文就来介绍一下关于 forEach 循环不知道的 8 个知识点。
1、不支持处理异步函数
const test = async () => {
let arrayNumbers = [3, 2, 1];
arrayNumbers.forEach(async (item) => {
const res = await mockSync(item);
console.log(res);
});
console.log("===> 结束");
};
const mockSync = (x) =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(x);
}, 1000 * x);
});
test();
从这段代码看,期望的输出为:
3
2
1
===> 结束
实际代码运行的输出如下:
===> 结束
1
2
3
JavaScript 中的 forEach()
方法是一个同步方法,不支持处理异步函数。如果在 forEach()
中执行异步函数,forEach()
无法等待异步函数完成,它将继续执行下一个项目。这意味着,如果在 forEach()
中使用异步函数,则无法保证异步任务的执行顺序。
如果要在循环中处理异步函数,则可以使用
map()
、filter()
、reduce()
和for
map()
、filter()
、reduce()
三个方法支持在函数中返回 Promise
,并会等待所有 Promise
完成。下面使用 map()
和Promise.all()
处理异步函数的示例代码如下:
const arrayNumbers = [1, 2, 3, 4, 5];
const asyncFunction = async (num) =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num * 2);
}, 1000);
});
const promises = arrayNumbers.map(async (num) => {
const result = await asyncFunction(num);
return result;
});
Promise.all(promises).then((results) => {
console.log(results); // [ 2, 4, 6, 8, 10 ]
});
上面的代码片段在 async
函数中使用了 await
关键字,map()
方法会等待 async
函数完成并返回结果,以便正确处理 async
函数。
下面再用 for
来实现,可以达到预期效果:
const arrayNumbers = [1, 2, 3, 4, 5];
const asyncFunction = async (num) =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num * 2);
}, 1000);
});
const processArray = async (arr) => {
const results = [];
for (let i = 0, len = arr.length; i < len; i++) {
const result = await asyncFunction(arr[i]);
results.push(result);
}
console.log(results); // [ 2, 4, 6, 8, 10 ]
};
processArray(arrayNumbers);
2、无法捕获异步函数中的错误
如果异步函数在执行时抛出错误,使用 forEach()
是无法捕获该错误。这意味着即使 async
函数发生错误,forEach()
也会继续执行。
3、除了抛出异常之外,没有办法中止或跳出 forEach() 循环
forEach()
方法不支持使用 break
或 continue
语句来中断循环或跳过项目。如果需要跳出循环或跳过某个项目,则应使用 for
循环或其他支持 break
或 continue
语句的方法。
下面是通过抛出异常方式退出循环:
const forEachExist = (array, callback, conditionFn) => {
try {
array.forEach((item) => {
if (conditionFn(item)) {
throw new Error("ExitLoop");
}
callback(item);
});
} catch (e) {
if (e.message !== "ExitLoop") {
throw e;
}
}
};
const arrayNumbers = [1, 2, 3, 4, 5, 6];
forEachExist(
arrayNumbers,
(item) => console.log(item),
(item) => item === 3
); // 输出:1 2
const arrayObjects = [
{
title: "文章1",
},
{ title: "文章2" },
];
forEachExist(
arrayObjects,
(item) => console.log(item),
(item) => item.title === "文章2"
); // { title: '文章1' }
4、forEach 删除自己的元素,索引无法重置
在 forEach()
中,无法控制 index
的值,它会无意识地增加,直到大于数组长度,跳出循环。因此,也不可能通过删除自身来重置索引。来看一个简单的例子:
let arrayNumbers = [1, 2, 3, 4];
arrayNumbers.forEach((item, index) => {
console.log(item); // 1 2 3 4
index++;
});
5、this 指向问题
在 forEach()
方法中,this
关键字指向调用该方法的对象。然而,当使用普通函数或箭头函数作为参数时,this
关键字的作用域可能会导致问题。在箭头函数中,this
关键字指向定义函数的对象。在普通函数中,this
关键字指向调用函数的对象。如果需要确保 this
关键字的作用域是正确的,可以使用 bind()
方法来绑定函数的作用域。下面是 forEach()
方法中 this
关键字作用域的问题示例:
const obj = {
name: "QuintionTang",
friends: ["Doman", "Raymon", "Dave"],
printFriends: function () {
this.friends.forEach(function (friend) {
console.log(`${this.name}是${friend}的朋友`);
});
},
};
obj.printFriends();
在这个例子中,定义了一个名为 obj
的对象,它有一个 printFriends()
方法。在 printFriends()
方法中,使用 forEach()
方法遍历 friends
数组,并使用普通函数打印每个朋友的名字和 obj
对象的 name
属性。运行代码输出如下:
undefined是Doman的朋友
undefined是Raymon的朋友
undefined是Dave的朋友
这是因为,在 forEach()
方法中使用普通函数时,函数的作用域不是调用 printFriends()
方法的对象,而是全局作用域。因此,无法在该函数中访问 obj
对象的属性。
要解决这个问题,可以使用 bind()
方法绑定函数作用域,或者使用箭头函数定义回调函数。
下面将使用箭头函数定义回调函数,则运行就达到预期了,如下:
const obj = {
name: "QuintionTang",
friends: ["Doman", "Raymon", "Dave"],
printFriends: function () {
this.friends.forEach((friend) => {
console.log(`${this.name}是${friend}的朋友`);
});
},
};
obj.printFriends();
代码输出结果如下:
QuintionTang是Doman的朋友
QuintionTang是Raymon的朋友
QuintionTang是Dave的朋友
还可以使用 bind()
方法解决问题的代码示例:
const obj = {
name: "QuintionTang",
friends: ["Doman", "Raymon", "Dave"],
printFriends: function () {
this.friends.forEach(
function (friend) {
console.log(`${this.name}是${friend}的朋友`);
}.bind(this)
);
},
};
obj.printFriends();
上面的代码通过使用 bind()
方法绑定函数作用域,可以正确访问 obj
对象的属性。
6、forEach 的性能低于 for 循环
for
:for
循环没有额外的函数调用栈和上下文,所以它的实现是最简单的。forEach()
:对于forEach
,其函数签名包含参数和上下文,因此性能会低于for
循环。for...of
:支持循环体中的各种控制流,如continue
、break
、yield
和await
。在效率上,for...of
比forEach()
快。
7、将跳过已删除或未初始化的项目
const array = [1, 2 /* empty */, , 4];
let num = 0;
array.forEach((ele) => {
console.log(ele);
num++;
});
// 1
// 2
// 4
for (let item of array) {
console.log(`for...of:${item}`);
}
// for...of:1
// for...of:2
// for...of:undefined
// for...of:4
console.log("num:", num); // num: 3
const words = ["one", "two", "three", "four"];
words.forEach((word) => {
console.log(word);
if (word === "two") {
words.shift();
}
}); // one // two // four
console.log(words); // ['two', 'three', 'four']
const words2 = ["one", "two", "three", "four"];
for (let item of words2) {
console.log(`for...of:${item}`);
if (item === "two") {
words2.shift();
}
}
console.log(words2); // [ 'two', 'three', 'four' ]
8、使用 forEach 不会改变原来的数组
调用 forEach()
时,它不会更改原始数组,即调用它的数组。但是那个对象可能会被传入的回调函数改变:
const array = [1, 2, 3, 4];
array.forEach((ele) => {
ele = ele * 3;
});
console.log(array); // [ 1, 2, 3, 4 ]
const numArr = [33, 4, 55];
numArr.forEach((ele, index, arr) => {
if (ele === 33) {
arr[index] = 999;
}
});
console.log(numArr); // [ 999, 4, 55 ]
// 2
const changeItemArr = [
{
name: "wxw",
age: 22,
},
{
name: "wxw2",
age: 33,
},
];
changeItemArr.forEach((ele) => {
if (ele.name === "wxw2") {
ele = {
name: "change",
age: 77,
};
}
});
console.log(changeItemArr); // [ { name: 'wxw', age: 22 }, { name: 'wxw2', age: 33 } ]
const allChangeArr = [
{ name: "wxw", age: 22 },
{ name: "wxw2", age: 33 },
];
allChangeArr.forEach((ele, index, arr) => {
if (ele.name === "wxw2") {
arr[index] = {
name: "change",
age: 77,
};
}
});
console.log(allChangeArr); // // [ { name: 'wxw', age: 22 }, { name: 'change', age: 77 } ]