JavaScript中的forEach,你踩过哪些坑?请避开这些常见误区

www.jswusn.com JS 2024-08-08 09:36:03 51次浏览



  在JavaScript的世界里,forEach是我们常用的数组遍历方法之一。大多数开发者都熟悉它的基础用法,但你知道吗?在处理异步操作时,forEach可能会让你掉进一些意想不到的“坑”。这篇文章将带你深入了解forEach的特性和局限,揭示一些你可能不知道的使用技巧和解决方案。无论你是前端新手,还是经验丰富的开发者,都能从中学到有用的知识,帮助你在实际项目中避开那些隐藏的陷阱。准备好了吗?让我们一探究竟!


先聊聊什么是forEach?


  forEach是数组对象的一个原型方法,它会为数组中的每个元素执行一次给定的回调函数,并且总是返回undefined。不过需要注意的是,类似arguments这样的类数组对象是没有forEach方法的哦。


基本语法

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

  别被这复杂的语法吓到,我们来逐个拆解。


参数详解

  1、callback:对每个元素执行的回调函数,它可以接受1到3个参数。

  •currentValue:当前处理的元素,必选。

  •index:当前处理元素的索引,可选。

  •array:正在操作的原数组对象,可选。

  2、thisArg:执行回调函数时this的值,默认为全局对象,可选。

1、forEach() 方法不支持处理异步函数

  在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关键字,确保每个异步操作完成后才处理下一个元素,从而实现了按顺序输出。

2、异步函数中的错误无法被捕获

  除了不能处理异步函数外,forEach还有另一个重要的限制:它无法捕获异步函数中的错误。这意味着即使异步函数在执行过程中抛出错误,forEach仍然会继续进行下一个元素的处理,而不会对错误进行处理。这种行为可能会导致程序出现意外的错误和不稳定性。

3、无法中断或跳过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。

4、无法删除自身元素并重置索引

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

5、this 关键字的作用域问题

  在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关键字指向正确的对象,从而正确访问对象的属性。

6、forEach 的性能低于 for 循环

  forEach方法虽然使用方便,但在性能方面却逊色于传统的for循环。原因在于forEach的函数签名包含参数和上下文,使得其性能低于for循环。

为什么 for 循环更快?

  1、简单实现:for循环的实现最为简单,没有额外的函数调用和上下文处理。

  2、减少函数调用栈:forEach方法每次迭代都会调用一次回调函数,增加了函数调用栈的开销。

  3、上下文处理:forEach方法需要处理函数的上下文和参数,这些操作都会消耗额外的时间和资源。

7、跳过已删除或未初始化的项

  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'。

8、不会改变原数组

  当调用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循环往往能提供更高的灵活性和性能。

技术分享

苏南名片

  • 联系人:吴经理
  • 电话:152-1887-1916
  • 邮箱:message@jswusn.com
  • 地址:江苏省苏州市相城区

热门文章

Copyright © 2018-2025 jswusn.com 版权所有

技术支持:苏州网站建设  苏ICP备18036849号