Array.prototype.forEach 方法详解

876 阅读6分钟

Array.prototype.forEach 方法详解

本文会从以下几个角度来对 forEach 进行解释:

  • forEach 方法的介绍
  • forEach 方法的特别之处
  • 如何跳出 forEach 循环
  • forEach 与 for 循环的区别

1.forEach 方法的介绍

forEach 首次提出于 ES3,并且在 ES5 中进行了完善。

forEach() 方法会对数组的每个元素都调用一次给定的 callbackFn 函数。

1.1 forEach 的参数与返回值

forEach 的参数有:

  • callbackFn
    • element
    • index
    • array
  • thisArg

forEach 返回值为 undefined(无返回值)。

1.1.1 callbackFn 必选参数

callbackFn 是一个必选参数,表示数组中每个元素所调用的函数。

函数调用时带有以下参数:

  1. element -> 数组中正在处理的当前元素。

  2. index -> 数组中正在处理的当前元素的索引。

  3. array -> forEach() 方法正在操作的数组。

1.1.2 thisArg 可选参数

我们使用 forEach 方法很少使用该参数,大家了解下即可:

  • thisArg 是一个可选参数,表示每次调用 callbackFn 函数时,this 的指向。

    • 如果省略了 thisArg 参数,或者其值为 null 或 undefined,this 则指向全局对象。
    • 使用该参数时,不要使用箭头函数,因为只有函数表达式声明的函数才有自己的 this 绑定

thisArg 使用示例如下:

//thisArg使用场景
class Counter {
  constructor() {
    this.sum = 0;
    this.count = 0;
  }
  add(array) {
    array.forEach(function countEntry(entry) {
      this.sum += entry; //元素值的总和
      ++this.count; //元素个数
    }, this);
  }
}
const obj = new Counter();
obj.add([2, 5, 9]);
console.log(obj.count); // 3
console.log(obj.sum); // 16

1.1.3 forEach 的返回值

forEach 必返回 undefined(无返回值)

1.2 调用 forEach 方法的语法格式

forEach 方法语法格式如下:

const arr = [1, 2, 3, 4];
// 回调函数
arr.forEach(callbackFn);
arr.forEach(callbackFn, thisArg);

// 内联回调函数
arr.forEach(function (element) {});
arr.forEach(function (element, index) {});
arr.forEach(function (element, index, array) {});
arr.forEach(function (element, index, array) {}, thisArg);

// 箭头函数
//使用箭头函数表达式来传入函数参数时, thisArg 参数会被忽略。
arr.forEach((element) => {});
arr.forEach((element, index) => {});
arr.forEach((element, index, array) => {});
//arr.forEach((element, index, array) => {},thisArg)
//此方法无法使用,因为箭头函数在词法上绑定了 this 值,函数里面没有this,而是使用了外层函数的this。

forEach 循环进行扁平化数组操作,代码如下:

//封装扁平化数组
const flatten = (arr) => {
  const result = [];
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  });
  return result;
};
const nested = [1, 2, 3, [4, 5, [6, 7], 8, 9]];
console.log(flatten(nested)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

2. forEach 的特别之处

  1. forEach() 遍历的范围在第一次调用 callbackFn 前就会确定,而且不会修改原数组。

  2. 另外 forEach() 不会在迭代之前创建数组的副本,所以:

    • 如果调用了 forEach 之后,再添加到数组中的元素不会被 callbackFn 访问到。
    • 如果迭代过程中改变了 itemN 的值或删除了 itemN 时:
      • itemN 被删除或未初始化(是空位)时,itemN 将不会被遍历,而会被跳过。
      • itemN 被修改时,传递给 callbackFn 的值是 forEach() 遍历到 itemN 那一刻的值。
      • 如果使用 shift、pop 等方法改变了数组中的结构,会对遍历结果有相应的影响,影响效果见下方代码。

数组的空位(empty item)的详解可参见阮一峰老师的 es6 入门

//在迭代时 使用shift方法删除了数组的头元素
const arr = [1, 2, 3, 4];
arr.forEach((item) => {
  console.log(item);
  if (item === 2) {
    arr.shift(); //1 将从数组中删除
  }
}); // 1 // 2 // 4
console.log(arr); // [2, 3, 4]
//在迭代时 使用pop方法删除了数组的尾元素
const arr = [1, 2, 3, 4];
arr.forEach((item) => {
  console.log(item);
  if (item === 2) {
    arr.pop(); //1 将从数组中删除
  }
}); // 1 // 2 // 3
console.log(arr); // [ 1, 2, 3 ]

3.如何跳出 forEach 循环

这是一道非常经典的面试题,考验了我们 js 的基础。

如果在 forEach 循环中想使用continue; break; return;这三个关键字来跳出循环时:

  • continue 报错: SyntaxError: Illegal continue statement: no surrounding iteration statement
  • break 报错: SyntaxError: Illegal break statement
  • return: 只能跳过一次循环,可认为相当于 for 循环中的 continue 关键字;

3.1 try catch + throw 方法

这是我们平时开发所使用的方式,该方法的算法效率为:O(n),性能较好。

//try catch + throw 方法
const arr = [1, 2, 3, 4];
try {
  arr.forEach((item) => {
    if (item === 2) {
      throw new Error(`值为${item}时跳出forEac循环`);
    }
    console.log(item); //只打印 1
  });
} catch (e) {
  console.log(e); //Error: 值为2时跳出forEac循环
}
console.log(arr); // [1, 2, 3, 4]

3.2 splice + return 方法

2. forEach 的特别之处的内容启发,所以我想到了该方法。

该方法的算法效率为:O(n²),性能较差。

至于两种方法的性能对比,可参考我的这篇博客JavaScript 中的 try catch 语句的性能分析

//splice + return 方法
//在迭代时 使用splice方法 删除数组中的元素
const arr = [1, 2, 3, 4];
let spliceArr = null;
arr.forEach((item, index) => {
  if (item === 2) {
    spliceArr = arr.splice(index); // 将 2 以后的元素全部删除 并赋值给spliceArr
    return;
  }
  console.log(item); // 1
});
arr.splice(arr.length, spliceArr.length, ...spliceArr); //将删除的元素拼接回去
console.log(arr); // [1, 2, 3, 4]

4.forEach 与 for 循环的区别

这也是一道经典的面试题,大家可以去看看这个视频面试官问:有了 for 循环为什么还要 forEach?

下面我将从本质,语法,性能三方面来进行讲解。

4.1 本质区别

  • for 循环是 js 刚提出时就有的循环方法,本质上就是一种基本语法。
  • forEach 是在 ES3 所提出,并在 ES5 中进行了功能完善,它是挂载在可迭代对象(Array Set Map 等)原型上的方法。
    • forEach 本质上是一个迭代器,负责遍历可迭代对象。

迭代器是 ES6 新增的一种特殊对象,它的标志是返回对象的 next()方法,迭代行为判断在 done 之中,能在不暴露内部表示的情况下,迭代器实现了遍历。 由于迭代器这块知识我尚未深入了解,所以暂时就不过多解释。

4.2 语法区别

  1. 参数的区别

    • arr.forEach(function (element, index, array) {}, thisArg)
    • for (initialization; expression; post-loop-expression) statement
  2. 中断方式的区别

    • for 循环可通过continue; break; return;这三个关键字来跳出循环
    • forEach 循环只能借助 try catch + throw 方法来跳出循环
  3. 在forEach对element进行的操作原则上不会修改原数组,除非直接使用index下标操作原数组。

  4. forEach 循环只能下标为 0 开始,不能进行认为干预,而 for 循环不同。

4.3 性能区别

  • 性能比较: for > forEach > map

在 chrome 62 和 Node.js v91.0 环境下:for 循环比 forEach 快 1 倍,forEach 比 map 快 20%左右。

  • 原因分析:

    • 因为 for 循环时没有涉及到函数调用栈执行上下文,所以它的性能最好。
    • 而 forEach 循环中执行了回调函数,会创建了对应的函数执行上下文,并会进行函数调用栈入栈出栈操作,还会涉及到垃圾回收机制,所以性能会低于 for 循环。
    • 而 map 循环最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会进行内存空间的分配操作,因此也会带来额外的性能损失。
  • 但抛开应用场景谈性能等于“耍流氓”,所以大家一定要结合实际应用场景来选择一种合适的方法。

结语

这是我目前所了解的知识面中最好的解答,当然也有可能存在一定的误区。

所以如果对本文存在疑惑,可以在评论区留言,我会及时回复的,欢迎大家指出文中的错误观点。

最后码字不易,觉得有帮助的朋友点赞、收藏、关注走一波。