關于 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 } ]