JavaScript中forEach的錯誤用法匯總

前言

使用過forEach的人大致有兩種:普通使用,簡簡單單;復雜使用,總想搞出點花樣來,結果一些莫名其妙的bug就出現瞭,解決這些bug所花費的時間都可以換一種思路實現瞭,能用作for循環的,又不隻是forEach。沒錯,筆者就是後者,終究是自己“學藝不精”。於是乎,花點時間,結合自己的實際開發經驗,再來好好理理forEach。

語法

forEach()是數組對象的一個原型方法,該方法會對數組中的每一個元素執行一次給定的回調函數,並且始終返回undefined。類數組對象是沒有forEach方法的,例如arguments。forEach的用法比較簡單:

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

實際例子:

const arr = [1,2,3];
arr.forEach(item => console.log(item)); // 1,2,3

參數說明:

callback:數組中每一個元素將要執行的回調函數,可以有1-3個參數

  • currentValue:當前正在處理的元素,必傳
  • index:當前正在處理的元素的索引,可選參數
  • array:forEach方法操作的原數組對象,可選參數

thisArg:當前執行callback回調函數時,回調函數的this指向,默認指向的全局對象,可選參數

語法看起來並不復雜,那麼看看是否會犯下面的錯誤用法:

錯誤用法

錯誤用法指的是標題所描述的操作,不是正文內容,切記,切記。

添加或刪除原數組中的數據

forEach()在第一次調用callback時就會確定遍歷的范圍,一般來說會按照索引升序為數組中的有效元素執行一次callback函數。如果是未初始化的數組項或者在調用forEach()後添加到數組中的項都不會被訪問到,如果在遍歷時刪除數組中的元素,則可能會出現意外的情況。

無效的值會直接跳過

調用後添加元素,將不會被訪問到

const arr = [1,,,2,4];
console.log(arr.length); // 5
let callbackCounts = 0;
arr.forEach(item => {
    console.log(item); // 1 2 4
    callbackCounts++;
    arr.push(6); // 調用後添加元素,將不會被訪問到
})
console.log(callbackCounts); // 3
console.log(arr); // [1,,,2,4,6,6,6]

刪除數組中的元素

const arr = ['a', 'b', 'c', 'd'];
console.log(arr.length); // 4
let callbackCounts = 0;
arr.forEach((item, index) => {
    // arr.shift();
    console.log(item); // 'a','b','d',其中'c'會被跳過
    if (item === 'b') {
        // arr.shift(); // 刪除頭部,arr的結果:['b', 'c', 'd']
        arr.splice(index, 1); // 刪除當前元素,arr的結果:['a', 'c', 'd']
        // arr.splice(-1); // 刪除最後一個元素,arr的結果:['a', 'b', 'c']
    }
    callbackCounts++;
})
console.log(callbackCounts); // 3
console.log(arr); // ['b', 'c', 'd']

刪除元素時情況可能會比較復雜一點,感興趣的朋友可以自己測試,我們可以這麼理解:

  • forEach在調用時就確定瞭遍歷范圍,傳遞給callback的值是forEach在遍歷到該元素時的值,即使在callback中刪除瞭數組中該元素,但是值已經傳遞進來
  • 如果是有效的值,callbackCounts會遞增。在下一輪循環時,forEach會根據上一輪循環時的索引得到當前循環對應的值,註意此時數組長度已經改變,就會出現“跳過”的現象
  • 然後重復操作,知道數組遍歷完畢

搞懂瞭”跳過“,你還敢想當然的刪除數組中的數據嗎?

修改原數組中的數據

既然不能添加和刪除,那我要是修改呢?其實修改數組中的元素,不是不可以,隻是要註意使用方法。

  • 如果數組中是基本數據類型:string、number、boolean等,隻使用回調函數的第一個參數修改數組中的值是不會影響原數組的

    const arr = [1,2,3,4,5]
    arr.forEach((item, index, array) => {
        item+=1; // [1,2,3,4,5]
        // arr[index]+=1; // [2,3,4,5,6]
        // array[index]+=1; // [2,3,4,5,6]
    })
    console.log(arr);
  • 如果數組中的是引用數據類型:object等,直接替換數組項是不會影響原數組的

    const arr = [
        {name: '張三', id: 1},
        {name: '李四', id: 2}
    ]
    arr.forEach((item, index, array) => {
        if (item.id === 2) {
            item = {name: '王五', id: item.id}; // 張三、李四
            // Object.assign(item, {name: '王五', id: item.id}); // 張三、王五
            // arr[index] = {name: '王五', id: item.id}; // 張三、王五
            // array[index] = {name: '王五', id: item.id}; // 張三、王五
        }
    })
    console.log(arr);

    數組對象在遍歷時,實際上是將數組項的引用地址賦值給item,如果將另一個對象的引用地址重新賦值給item,並不會改變原引用地址的數據,也就不會影響原數組。

  • 如果數組中的是引用數據類型:object等,此時我們隻修改數組項的某一個屬性,這個時候是會影響原數組的

    const arr = [
        {name: '張三', id: 1},
        {name: '李四', id: 2}
    ]
    arr.forEach((item, index, array) => {
    if (item.id === 2) {
        item.name = '王五';
        // arr[index].name = '王五'; // 張三、王五
        // array[index].name = '王五'; // 張三、王五
    }
    })
    console.log(arr); // 張三、王五

    道理呢也和2類似,item指向的是引用地址,修改屬性相當於是修改瞭引用地址中對象的屬性,也就會修改原數組

綜上我們可以發現,如果要在forEach中修改原數組,那麼需要在其回調函數中,通過索引index或者借助Object.assgin()才可以實現,最終原理都是修改引用地址中的數據,而不是直接修改。

回調函數中使用異步函數

異步函數和同步函數的執行順序此處就不細說,詳情可以移步:搞不清楚事件循環,那就看看這篇文章,簡單來說就是同步代碼先於異步代碼執行。

看一個例子:

const arr = [1,2,3,4,5]
let sum = 0;
let callbackCounts = 0;
function Sum(a, b) {
    return new Promise((resovle) => {
        resovle(a + b)
    })
}
arr.forEach(async (item, index, array) => {
    sum = await Sum(sum, item)
})
console.log(sum); // 0

實際得到的求和的值並不是我們期待的15,而是0。

如果我們需要實現異步求和,可以使用for循環實現:

const arr = [1,2,3,4,5]
let sum = 0;
let callbackCounts = 0;
function Sum(a, b) {
    return new Promise((resovle) => {
        resovle(a + b)
    })
}
(async function() {
    for (let item of arr) {
        sum = await Sum(sum, item)
    }
    console.log(sum); // 15
})();

使用return結束循環

在使用for循環時,我們一般可使用breakreturn來跳出循環,拋出異常也可以,但是這不是正常的開發流程。我們來試一下在forEach中使用break、return有沒有作用:

forEach結束循環

const arr = [1,2,3,4,5]
let callbackCounts = 0;
arr.forEach((item, index, array) => {
    callbackCounts++;
        if (item === 2) {
        return; // forEach中不能使用break,即使使用return,也無法中止循環
    }
})
console.log(arr.length, callbackCounts); // 5 5

如果非得要跳出forEach循環,首先建議的是使用其他循環方法,例如:for、for of、for in、map等,其次我們可以考慮拋出一個異常來跳出forEach循環:

const arr = [1,2,3,4,5]
let callbackCounts = 0;
try {
    arr.forEach((item, index, array) => {
        callbackCounts++;
        if (item === 2) {
            throw 'throw forEach';
        }
    })
} catch (e) {
    console.log(arr.length, callbackCounts); // 5 2
}

如果真要使用throw來拋出異常,那麼使用其他循環方法不香嗎

未傳入this

forEach()也可能存在this指向問題,例如:

function Counter() {
    this.sum = 0;
}
Counter.prototype.add = function (array) {
    array.forEach(function(element) {
        this.sum += element;
    });
}
const obj = new Counter();
obj.add([1,2,3,4,5])
console.log(obj.sum); // 0

未指定this,則默認未window對象,此時的this.sum為undefined,而我們想的是this指向傳入的數組。那麼需要傳入this或者使用箭頭函數。

array.forEach((element) => {
    this.sum += element;
});
array.forEach(function(element) {
    this.sum += element;
}, this);

正確用法

避免錯誤用法,當然就是正確用法咯。

其實forEach在設計出來隻是為瞭簡化for循環的遍歷,如果要過多的進行其他操作,就違背瞭設計初衷瞭。每一個API都有自己的適用范圍,如果堅持要一把梭,可能就會踩很多坑。

簡單總結一下正確的使用方法:

  • 最好隻限於遍歷原數組,不涉及修改原數組中的數據
  • 避免在回調函數中存在異步操作
  • 不能使用return
  • forEach()的回調函數使用箭頭函數,可避免this指向問題

總結

  • forEach本身並不會改變原數組,但是其回調函數可能會修改。如果真要修改原數組,建議使用mapfilter等方法
  • forEach方法始終返回undefined,這使得forEach無法像mapfilter一樣可以鏈式調用
  • 除瞭拋出異常外,forEach無法被中止或者跳出循環,如果要跳出循環,建議使用其他for循環方法
  • 如果是涉及異步函數,可以考慮使用for await of代替forEach

到此這篇關於JavaScript中forEach錯誤用法的文章就介紹到這瞭,更多相關js forEach錯誤用法內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: