关于 JavaScript 中的 forEach 循环你不知道的 8 件事

关于 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() 方法不支持使用 breakcontinue 语句来中断循环或跳过项目。如果需要跳出循环或跳过某个项目,则应使用 for 循环或其他支持 breakcontinue 语句的方法。

下面是通过抛出异常方式退出循环:

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 循环

  • forfor 循环没有额外的函数调用栈和上下文,所以它的实现是最简单的。
  • forEach():对于 forEach,其函数签名包含参数和上下文,因此性能会低于 for 循环。
  • for...of:支持循环体中的各种控制流,如 continuebreakyieldawait。在效率上,for...offorEach() 快。

 

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