JavaScript的世界里,forEach是我們常用的數組遍歷方法之一。大多數開發者都熟悉它的基礎用法,但你知道嗎?在處理異步操作時,forEach可能會讓你掉進一些意想不到的“坑”。這篇文章將帶你深入了解forEach的特性和局限,揭示一些你可能不知道的使用技巧和解決方案。無論你是前端新手,還是經驗豐富的開發者,都能從中學到有用的知識,幫助你在實際項目中避開那些隱藏的陷阱。準備好了嗎?讓我們一探究竟!
forEach是數組對象的一個原型方法,它會為數組中的每個元素執行一次給定的回調函數,并且總是返回undefined。不過需要注意的是,類似arguments這樣的類數組對象是沒有forEach方法的哦。
基本語法
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
別被這復雜的語法嚇到,我們來逐個拆解。
參數詳解
在JavaScript中,forEach() 是一個同步方法,不支持處理異步函數。如果你在 forEach 中執行一個異步函數,forEach 不會等待異步函數完成,而是會立即處理下一個元素。這意味著如果你在 forEach 中使用異步函數,異步任務的執行順序是無法保證的。
示例代碼
async function test() {
let arr=[3, 2, 1];
arr.forEach(async item=> {
const res=await mockAsync(item);
console.log(res);
});
console.log('end');
}
function mockAsync(x) {
return new Promise((resolve, reject)=> {
setTimeout(()=> {
resolve(x);
}, 1000 * x);
});
}
test();
預期結果:
3
2
1
end
實際結果:
end
1
2
3
這個例子中,雖然我們希望按順序輸出 3, 2, 1 和 end,但實際結果是 end 先輸出,然后才是 1, 2, 3。這是因為 forEach 不等待異步操作完成。
解決方法:使用 for...of 循環和 async/await
為了解決這個問題,我們可以使用 for...of 循環和 async/await 關鍵字來確保異步操作按順序完成。
示例代碼
async function test() {
let arr=[3, 2, 1];
for (let item of arr) {
const res=await mockAsync(item);
console.log(res);
}
console.log('end');
}
function mockAsync(x) {
return new Promise((resolve, reject)=> {
setTimeout(()=> {
resolve(x);
}, 1000 * x);
});
}
test();
輸出結果:
3
2
1
end
在這個例子中,我們使用 for...of 循環代替 forEach 方法,通過在循環內部使用 await 關鍵字,確保每個異步操作完成后才處理下一個元素,從而實現了按順序輸出。
除了不能處理異步函數外,forEach還有另一個重要的限制:它無法捕獲異步函數中的錯誤。這意味著即使異步函數在執行過程中拋出錯誤,forEach 仍然會繼續進行下一個元素的處理,而不會對錯誤進行處理。這種行為可能會導致程序出現意外的錯誤和不穩定性。
除了無法處理異步函數和捕獲錯誤之外,forEach還有一個限制:它不支持使用break或continue語句來中斷或跳過循環。如果你需要在循環中途退出或跳過某個元素,應該使用其他支持這些語句的方法,例如for循環。
示例代碼
let arr=[1, 2, 3];
try {
arr.forEach(item=> {
if (item===2) {
throw('error');
}
console.log(item);
});
} catch(e) {
console.log('e:', e);
}
// 輸出結果:
// 1
// e: error
在這個例子中,我們嘗試通過拋出異常來中斷forEach循環。雖然這種方法在某些情況下有效,但并不是優雅或推薦的做法。
更好的解決方案:使用 for...of 循環
相比之下,for...of 循環更靈活,可以使用break和continue語句來控制循環的執行。
示例代碼
let arr=[1, 2, 3];
for (let item of arr) {
if (item===2) {
break; // 中斷循環
}
console.log(item);
}
// 輸出結果:
// 1
在這個例子中,當遇到元素2時,循環會被中斷,從而避免輸出2和3。
在forEach中,我們無法控制索引的值,它只是盲目地遞增直到超過數組的長度并退出循環。因此,刪除自身元素以重置索引也是不可能的。來看一個簡單的例子:
示例代碼
let arr=[1, 2, 3, 4];
arr.forEach((item, index)=> {
console.log(item); // 輸出: 1 2 3 4
index++;
});
在這個例子中,forEach遍歷數組 arr,輸出每個元素的值。雖然我們嘗試在循環內部遞增 index,但這并不會影響forEach的內部機制。forEach中的索引是自動管理的,并且在每次迭代時都會自動遞增。
為什么無法刪除元素并重置索引?
在forEach中,索引的值是由forEach方法內部控制的。即使我們手動修改索引變量,也不會影響forEach的遍歷行為。更具體地說,當我們試圖在forEach內部刪除元素時,forEach不會重新計算索引,這會導致一些元素被跳過,或者某些情況下出現未定義的行為。
例如,如果我們嘗試刪除當前元素:
錯誤示范
let arr=[1, 2, 3, 4];
arr.forEach((item, index)=> {
if (item===2) {
arr.splice(index, 1); // 嘗試刪除元素2
}
console.log(item); // 輸出: 1 2 4
});
console.log(arr); // 輸出: [1, 3, 4]
在這個例子中,當我們刪除元素2時,forEach并不會重置或調整索引,因此它繼續處理原數組中的下一個元素。這導致元素3被跳過,因為原來的元素3現在變成了元素2的位置。
當元素 2 被刪除后,原數組變為 [1, 3, 4],forEach會繼續按照原索引順序進行,因此輸出 1, 2, 4,而元素 3 被跳過了。這是因為元素 3 在 2 被刪除后移動到了索引 1 的位置,而forEach的索引已經移動到 2,所以直接輸出了刪除后的索引 2 位置的新元素 4。
更好的解決方案:使用for循環
let arr=[1, 2, 3, 4];
for (let i=0; i < arr.length; i++) {
if (arr[i]===2) {
arr.splice(i, 1); // 刪除元素2
i--; // 調整索引
} else {
console.log(arr[i]); // 輸出: 1 3 4
}
}
console.log(arr); // 輸出: [1, 3, 4]
在forEach方法中,this關鍵字指的是調用該方法的對象。然而,當我們使用常規函數或箭頭函數作為參數時,this關鍵字的作用域可能會出現問題。在箭頭函數中,this關鍵字指的是定義該函數的對象;而在常規函數中,this關鍵字指的是調用該函數的對象。為了確保this關鍵字的正確作用域,我們可以使用bind方法來綁定函數的作用域。以下是一個說明this關鍵字作用域問題的例子:
示例代碼
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach(function (friend) {
console.log(this.name + " is friends with " + friend);
});
},
};
obj.printFriends();
在這個例子中,我們定義了一個名為obj的對象,里面有一個printFriends方法。我們使用forEach方法遍歷friends數組,并使用常規函數來打印每個朋友的名字和obj對象的name屬性。然而,運行這段代碼時,輸出如下:
undefined is friends with Bob
undefined is friends with Charlie
undefined is friends with Dave
這是因為在forEach方法中使用常規函數時,該函數的作用域不是調用printFriends方法的對象,而是全局作用域。因此,無法訪問obj對象的屬性。
使用bind方法解決
為了解決這個問題,我們可以使用bind方法來綁定函數的作用域,將其綁定到obj對象。下面是一個使用bind方法解決問題的例子:
示例代碼
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach(
function (friend) {
console.log(this.name + " is friends with " + friend);
}.bind(this) // 使用bind方法綁定函數的作用域
);
},
};
obj.printFriends();
運行這段代碼,輸出如下:
Alice is friends with Bob
Alice is friends with Charlie
Alice is friends with Dave
通過使用bind方法綁定函數的作用域,我們可以正確地訪問obj對象的屬性。
使用箭頭函數解決
另一個解決方案是使用箭頭函數。由于箭頭函數沒有自己的this,它會繼承其當前作用域的this。因此,在箭頭函數中,this關鍵字指的是定義該函數的對象。
示例代碼
const obj={
name: "Alice",
friends: ["Bob", "Charlie", "Dave"],
printFriends: function () {
this.friends.forEach((friend)=> {
console.log(this.name + " is friends with " + friend);
});
},
};
obj.printFriends();
運行這段代碼,輸出如下:
Alice is friends with Bob
Alice is friends with Charlie
Alice is friends with Dave
使用箭頭函數,我們可以確保this關鍵字指向正確的對象,從而正確訪問對象的屬性。
forEach 方法雖然使用方便,但在性能方面卻遜色于傳統的 for 循環。原因在于 forEach 的函數簽名包含參數和上下文,使得其性能低于 for 循環。
為什么 for 循環更快?
forEach方法在遍歷數組時會跳過未初始化的值和已刪除的值。這可能會導致一些意想不到的行為。
跳過未初始化的值
在數組中,如果某些值未初始化,forEach會直接跳過這些值。來看下面這個例子:
const array=[1, 2, /* 空 */, 4];
let num=0;
array.forEach((ele)=> {
console.log(ele);
num++;
});
console.log("num:", num);
// 輸出結果:
// 1
// 2
// 4
// num: 3
在這個例子中,數組中的第三個元素未初始化,forEach直接跳過了它。因此,雖然數組的長度是4,但實際被遍歷的元素只有3個。
跳過已刪除的值
當在forEach循環中刪除數組元素時,forEach會跳過這些已刪除的值。來看下面這個例子:
const words=['one', 'two', 'three', 'four'];
words.forEach((word)=> {
console.log(word);
if (word==='two') {
words.shift(); // 刪除數組中的第一個元素 'one'
}
});
// 輸出結果:
// one
// two
// four
console.log(words); // ['two', 'three', 'four']
在這個例子中,當遍歷到元素 'two' 時,執行了 words.shift(),刪除了數組中的第一個元素 'one'。由于數組元素向前移動,元素 'three' 被跳過,forEach 直接處理新的第三個元素 'four'。
當調用forEach方法時,它不會改變原數組,即它被調用的數組。然而,傳遞的回調函數可能會改變數組中的對象。
示例代碼1
const array=[1, 2, 3, 4];
array.forEach(ele=> { ele=ele * 3 })
console.log(array); // [1, 2, 3, 4]
在這個例子中,forEach方法并沒有改變原數組。雖然在回調函數中對每個元素進行了乘3的操作,但這些操作并沒有反映在原數組中。
如果希望通過forEach改變原數組,需要直接修改數組元素的值,而不是簡單地對元素進行賦值。
示例代碼
const numArr=[33, 4, 55];
numArr.forEach((ele, index, arr)=> {
if (ele===33) {
arr[index]=999;
}
});
console.log(numArr); // [999, 4, 55]
在這個例子中,我們通過forEach方法直接修改了數組中的元素,從而改變了原數組。
示例代碼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}]
在這個例子中,通過索引直接修改數組中的對象,從而實現了對原數組的修改。
總結一下,forEach雖然方便,但在一些特定場景下,使用傳統的for循環或其他遍歷方法可能更適合你的需求。比如,當你需要精確控制循環流程、處理異步操作或是修改原數組時,for循環往往能提供更高的靈活性和性能。
使用for循環的時候可以使用break 或者return語句來結束for循環(return直接結束函數),但是如果使用forEach循環如何跳出循環呢?
聽說視頻配文檔更容易理解,??
首先嘗試一使用return語句----木有效果
[1,2,3,4,5].forEach(item=>{ if(item===2){ return } console.log(item); })
在嘗試一下使用break語句----報錯
[1,2,3,4,5].forEach(item=>{ if(item===2){ break } console.log(item); })
為什么會出現這樣的情況?先看一下官方文檔的說明。MDN文檔上明確說明forEach循環是不可以退出的。
引自MDN
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.
注意: 沒有辦法中止或者跳出 forEach() 循環,除了拋出一個異常。如果你需要這樣,使用 forEach() 方法是錯誤的。
若你需要提前終止循環,你可以使用:
簡單循環
for...of 循環
Array.prototype.every()
Array.prototype.some()
Array.prototype.find()
Array.prototype.findIndex()
先看看為什么return沒有效果,break報錯,forEach的實現方式用代碼表示出來可以寫成如下的結構
const arr=[1, 2, 3, 4, 5]; for (let i=0; i < arr.length; i++) { const rs=(function(item) { console.log(item); if (item > 2) return false; })(arr[i]) }
使用return語句相當于在每個自執行函數中將返回值復制給rs,但是實際對整個函數并沒有影響。而使用break語句報錯是因為再JS的解釋器中break語句是不可以出現在函數體內的。
MDN官方推薦的方法
every在碰到return false的時候,中止循環。some在碰到return ture的時候,中止循環。 var a=[1, 2, 3, 4, 5] a.every(item=>{ console.log(item); //輸出:1,2 if (item===2) { return false } else { return true } }) var a=[1, 2, 3, 4, 5] a.some(item=> { console.log(item); //輸出:1,2 if (item===2) { return true } else { return false } })
其他方法
1.使用for循環或者for in 循環代替
2.使用throw拋出異常
try { [1, 2, 3, 4, 5].forEach(function(item) { if (item===2) throw item; console.log(item); }); } catch (e) {}
3.使用判斷跑空循環
var tag; [1, 2, 3, 4, 5].forEach(function(item){ if(!tag){ console.log(item); if(item===2){ tag=true; } } })
這樣做有兩個問題,第一個問題,全局增加了一個tag變量,第二個問題,表面上看是終止了forEach循環,但是實際上循環的次數并沒有改變,只是在不滿足條件的時候callback什么都沒執行而已,先來解決第一個問題,如何刪除全局下新增的tag變量 。實際上forEach還有第二個參數,表示callback的執行上下文,也就是在callback里面this對應的值。因此我們可以講上下文設置成空對象,這個對象自然沒有tag屬性,因此訪問this.tag的時候會得到undefined
[1, 2, 3, 4, 5].forEach(function(item){ if(!this.tag){ console.log(item); if(item===2){ this.tag=true; } } },{})
4.修改索引
var array=[1, 2, 3, 4, 5] array.forEach(item=>{ if (item==2) { array=array.splice(0); } console.log(item); })
講解:
forEach的執行細節
1.遍歷的范圍在第一次執行callback的時候就已經確定,所以在執行過程中去push內容,并不會影響遍歷的次數,這和for循環有很大區別,下面的兩個案例一個會造成死循環一個不會
var arr=[1,2,3,4,5] //會造成死循環的代碼 for(var i=0;i<arr.length;i++){ arr.push('a') } //不會造成死循環 arr.forEach(item=>arr.push('a'))
2.如果已經存在的值被改變,則傳遞給 callback 的值是 forEach 遍歷到他們那一刻的值。
var arr=[1,2,3,4,5]; arr.forEach((item,index)=>{ console.log(`time ${index}`) arr[index+1]=`${index}a`; console.log(item) })
3.已刪除的項不會被遍歷到。如果已訪問的元素在迭代時被刪除了(例如使用 shift()),之后的元素將被跳過。
var arr=[1,2,3,4,5]; arr.forEach((item,index)=>{ console.log(item) if(item===2){ arr.length=index; } })
在滿足條件的時候將后面的值截掉,下次循環的時候照不到對應的值,循環就結束了,但是這樣操作會破壞原始的數據,因此我們可以使用一個小技巧,即將數組從0開始截斷,然后重新賦值給數組也就是array=array.splice(0)
本期教程資料請點擊更多下載,提取碼: 558x
良心教程,歡迎關注評論,或者訪問我們的官網http://www.bingshangroup.com
和我們交流。
當時就直接回他:“不能,我做不到。”
結果呢,這句話就像按了快進鍵,面試官突然宣布面試結束。
心里那個郁悶啊,我就問面試官:“這有啥不對嗎?難道真的有辦法在JavaScript中讓forEach歇菜嗎?”
還沒等他回我,我就開始自我解惑,說出了我認為forEach不能停的理由。
我的小伙伴們,猜猜看,下面這段代碼會打印出什么數字?
會只打印一個數字,還是一串數字?
正確答案是,它會打印出‘0’、‘1’、‘2’、‘3’。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it)=> {
if (it >=0) {
console.log(it)
return // or break
}
})
是的!我把這代碼拿給面試官看,但他還是堅持認為JavaScript的forEach循環是可以停的。
哦天啊,開什么國際玩笑呢。
要讓他信服,我就得再來一次forEach的模擬。
Array.prototype.forEach2=function (callback, thisCtx) {
if (typeof callback !=='function') {
throw `${callback} is not a function`
}
const length=this.length
let i=0
while (i < length) {
if (this.hasOwnProperty(i)) {
// Note here:Each callback function will be executed once
callback.call(thisCtx, this[ i ], i, this)
}
i++
}
}
確實,當我們用forEach遍歷數組時,每個元素都要跑一遍回調函數,早退門都沒有。
比如說,下面這段代碼里,就算func1遇到了break,控制臺還是會打印出‘2’。
const func1=()=> {
console.log(1)
return
}
const func2=()=> {
func1()
console.log(2)
}
func2()
你很棒,但我得告訴你,至少有三種方法可以讓JavaScript里的forEach停止。
找到第一個大于或等于0的數字后,代碼就進入死胡同了。所以控制臺只會跟你說個0。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
try {
array.forEach((it)=> {
if (it >=0) {
console.log(it)
throw Error(`We've found the target element.`)
}
})
} catch (err) {
}
哦!我的個神啊!我簡直不敢相信,都快說不出話來。
別這么驚訝,面試官跟我說。
咱們還可以通過把數組長度設置成0來讓forEach打卡下班。你也知道,數組沒了,forEach自然也就不跑了。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it)=> {
if (it >=0) {
console.log(it)
array.length=0
}
})
哦!天哪,我的腦子都亂套了。
這招和第二招一個味兒,如果能把目標元素后面的值都給刪了,forEach也就自動停工了。
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.forEach((it, i)=> {
if (it >=0) {
console.log(it)
// Notice the sinful line of code
array.splice(i + 1, array.length - i)
}
})
我瞪大了眼睛,真不想看這代碼。太傷眼了。
最后我跟面試官說:“哎,可能你說的對,你確實找到了停forEach的方法,但要是用這種代碼,我覺得你們老板遲早得讓你走人。”
或許咱們應該考慮用for循環或者some方法來解決問題。
1. for
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
for (let i=0, len=array.length; i < len; i++) {
if (array[ i ] >=0) {
console.log(array[ i ])
break
}
}
2. some
const array=[ -3, -2, -1, 0, 1, 2, 3 ]
array.some((it, i)=> {
if (it >=0) {
console.log(it)
return true
}
})
感謝您的閱讀!如果您對本文有任何疑問或想要分享您的看法,請隨時通過私信或在下方評論區與我交流。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。