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