1. forEach 的基本用法
1.1 回调函数的参数
在 JavaScript forEach 遍历数组中,回调函数会被逐一调用,接收 三个参数:当前元素的值、元素的索引、以及原始数组本身。这三个参数的组合让你可以在回调中实现对比、聚合、筛选等多种操作。
通过这三个参数,你可以在回调中进行对比、统计、累加等操作,并且不会改变遍历的语义。通常最常用的是 value 和 index,如果需要了解原数组的长度或结构,第三个参数 array 也会派上用场。
const nums = [10, 20, 30];
nums.forEach((value, index, array) => {console.log(value, index, array.length);
});
在这里 当前元素的值与 索引是最常用的两个参数,第三个参数通常用于需要原数组信息的场景。
本节的核心概念是:forEach 将回调应用于数组的每个可迭代项,但并不返回新的数据结构。
1.2 返回值与副作用
与 map、filter 等方法不同,forEach 的返回值始终是 undefined,因此你不能用它来获得一个新数组或转化结果。
如果需要累积结果,通常要在外部声明变量,或使用 reduce 来完成链式转换。这也是为什么 forEach 多用于副作用操作而非数据变换。
let sum = 0;
[1,2,3,4].forEach(n => { sum += n; });
console.log(sum); // 10
需要明确的是,对外部变量的修改属于副作用,而 forEach 本身并不返回值去描述这个副作用。
2. forEach 的工作原理
2.1 同步执行与异常传播
在 JavaScript 的执行模型中,forEach 是同步执行的。也就是说,回调函数在当前事件循环内逐个调用,完成后才继续执行后续代码。
如果回调抛出异常,循环会被中断,后续元素的回调不会再被触发,这也是需要在实际使用中注意的行为。
[1,2,3].forEach(n => {if (n === 2) throw new Error('stop');console.log(n);
});
因此在需要灵活控制遍历中止条件时,务必在回调中处理异常或使用其他控制流方式来实现。
2.2 稀疏数组与 holes
对于稀疏数组,forEach 不会对未初始化的元素进行回调, holes 不会被访问,这点与普通循环不同。
如果需要覆盖所有索引,包括空洞,可以使用普通 for 循环或使用 Array.from、for...of 等替代方案,将稀疏数组转化为完整序列再遍历。
const arr = [1,,3];
arr.forEach((v, i) => console.log(i, v)); // 输出 0 1,2 3(第2个是空洞,不会输出)
理解这一点很重要,因为在数据清洗或填充缺失值的场景下,遍历行为的差异可能影响结果。
2.3 thisArg 的作用
forEach 接受一个可选的第二个参数 thisArg,用于绑定回调中的 this 指向。如果不提供 thisArg,回调中的 this 通常为 undefined(在严格模式下)或全局对象。
通过传入对象,你可以在回调中访问到外部的状态,避免额外的闭包创建。使用 thisArg 可以让回调更灵活地访问上下文。
const ctx = { factor: 2 };
[1, 2, 3].forEach(function(n) {console.log(n * this.factor);
}, ctx);3. 使用场景与边界
3.1 与替代方案的关系
在需要得到新数组的场景,优先考虑 map,它会返回一个新数组且不改变原数组;filter 用于按条件筛选,reduce 可以把数组简化为任意形态的值。
如果目标仅是执行副作用或累积外部变量,forEach 是合适的工具,但请避免把它当成纯函数来产出新数据。
const doubled = [1,2,3].map(n => n * 2); // [2,4,6]
const evens = [1,2,3,4].filter(n => n % 2 === 0);
const sum = [1,2,3,4].reduce((acc, v) => acc + v, 0);
本节强调的是:forEach 适合“逐项执行副作用”而非“产生新数据结构”,这与前端开发中常见的数据流模式密切相关。
3.2 对对象和类数组的遍历
对类数组对象,通常需要先转换为真正的数组再使用 forEach,或者用 Array.prototype.forEach.call 来遍历。Array.from 或 Object.values/Object.keys 常用于构建遍历目标。
function logLengths() {const items = { a: 'foo', b: 'bar' };Object.values(items).forEach(v => console.log(v.length));
}
logLengths();4. 实战技巧与性能考虑
4.1 将 forEach 与对象、节点列表一起使用
在前端开发中,常见场景包括遍历节点集合或对象属性,forEach 提供直观的遍历语义,提高代码可读性。
如果你在处理 DOM 节点集合,如 NodeList,确保浏览器对 NodeList.forEach 的支持;否则可以先转换为数组再遍历。
const nodes = document.querySelectorAll('p');
nodes.forEach(p => {p.style.color = '#333';
});4.2 性能注意与替代策略
在处理巨量数据时,for 循环在某些场景下可能更省略开销;对比 forEach,性能差异通常微小,但在极端性能敏感的场景中,手写循环仍然是基准线。
此外,避免在回调中进行耗时的异步操作,将复杂逻辑提取到独立函数,必要时分段执行以避免 UI 阻塞。

// 尽量把复杂逻辑提取到函数中
function process(n) { /* 复杂逻辑 */ }
[1,2,3,4].forEach(process); 

