forEach 和 map 都是 JavaScript 中遍历数组的常用方法,核心差异在于返回值 和设计用途 :forEach 专注「遍历执行操作」(无返回值),map 专注「数据转换」(返回新数组)。以下是全方位对比,帮你精准区分和使用。
一、核心差异(最关键)
| 特性维度 | forEach | map |
|---|---|---|
| 返回值 | 始终返回 undefined(无任何返回值) |
返回一个新数组(长度与原数组一致,元素为回调函数的返回值) |
| 核心用途 | 遍历数组,执行"无返回值"的操作(如打印、修改 DOM、更新原数组元素) | 遍历数组,对元素做"转换/映射",生成新数组(如格式化数据、提取属性) |
| 原数组修改 | 不会直接改变原数组,但回调内可修改原数组元素(引用类型) | 不会直接改变原数组,新数组是独立的;回调内也可修改原数组元素(同 forEach) |
| 链式调用 | 不支持(返回 undefined,无法链式调用 filter/reduce 等) |
支持(返回新数组,可链式调用其他数组方法) |
| 中断遍历 | 无法中断(break/return 仅退出当前回调,无法终止整个遍历;仅能抛异常终止,不推荐) |
无法中断(同上) |
| 性能 | 略优(无创建新数组的开销) | 略逊(需创建新数组,现代浏览器优化后差异极小) |
二、代码示例:直观感受差异
示例 1:返回值差异(核心)
javascript
const arr = [1, 2, 3];
// 1. forEach:无返回值
const resForEach = arr.forEach(item => {
return item * 2; // 返回值无意义,forEach 会忽略
});
console.log(resForEach); // undefined
console.log(arr); // [1,2,3](原数组未变)
// 2. map:返回新数组
const resMap = arr.map(item => {
return item * 2; // 回调返回值作为新数组元素
});
console.log(resMap); // [2,4,6](新数组)
console.log(arr); // [1,2,3](原数组仍未变)
示例 2:链式调用差异
map 可配合 filter/reduce 等实现复杂数据处理,forEach 不行:
javascript
const arr = [1, 2, 3, 4, 5];
// map 链式调用:先翻倍 → 筛选大于5的数 → 求和
const result = arr
.map(item => item * 2) // [2,4,6,8,10]
.filter(item => item > 5) // [6,8,10]
.reduce((sum, item) => sum + item, 0); // 24
console.log(result); // 24
// forEach 无法链式调用(报错)
// arr.forEach(item => item * 2).filter(...)
// Uncaught TypeError: Cannot read properties of undefined (reading 'filter')
示例 3:修改原数组元素(引用类型)
两者都不会"直接改变原数组",但如果数组元素是引用类型(对象/数组),回调内修改元素属性会影响原数组:
javascript
const userList = [{ name: '张三', age: 18 }, { name: '李四', age: 20 }];
// forEach 修改原数组元素属性
userList.forEach(user => {
user.age += 1; // 修改引用类型的属性
});
console.log(userList); // [{name:'张三',age:19}, {name:'李四',age:21}]
// map 同样会修改原数组元素属性(但返回新数组)
const newUserList = userList.map(user => {
user.age += 1; // 原数组元素被修改
return user; // 新数组元素是原对象的引用
});
console.log(userList); // [{name:'张三',age:20}, {name:'李四',age:22}]
console.log(newUserList); // 与修改后的 userList 指向同一对象
// 🌟 避免修改原数组:map 中返回新对象(浅拷贝)
const newUserList2 = userList.map(user => ({
...user, // 浅拷贝原对象
age: user.age + 1 // 新属性值,不影响原对象
}));
console.log(userList); // 原数组不变
console.log(newUserList2); // 新对象数组
三、常见误区澄清
误区 1:map 会改变原数组
❌ 错误:map 本身不会修改原数组,仅返回新数组;若原数组被修改,是因为回调内主动修改了引用类型元素的属性(forEach 同理)。
✅ 正确:如需纯数据转换且不影响原数组,在 map 中返回新对象/新值(如示例 3 的 newUserList2)。
误区 2:forEach 可以中断遍历
❌ 错误:break/return 在 forEach 回调内仅退出当前循环,无法终止整个遍历;只有抛出异常能终止(不推荐)。
✅ 正确:如需中断遍历,用 for 循环/for...of(支持 break),而非 forEach/map。
误区 3:map 性能远差于 forEach
❌ 错误:现代浏览器对 map 做了优化,仅在百万级超大数组 下 forEach 略快,日常开发(千/万级数据)差异可忽略,优先按"是否需要返回新数组"选型。
四、选型建议(一句话记住)
| 场景需求 | 选 forEach | 选 map |
|---|---|---|
| 仅遍历执行操作(打印、改 DOM、改原数组属性) | ✅ | ❌ |
| 需要转换数据生成新数组(格式化、提取属性、链式操作) | ❌ | ✅ |
典型场景示例
javascript
// 场景 1:遍历打印(选 forEach)
const arr = [1,2,3];
arr.forEach(item => console.log(item)); // 1 2 3
// 场景 2:提取对象数组的属性(选 map)
const userList = [{name:'张三'}, {name:'李四'}];
const nameList = userList.map(user => user.name);
console.log(nameList); // ['张三', '李四']
// 场景 3:格式化数据(选 map)
const numArr = [1,2,3];
const formatArr = numArr.map(num => `¥${num.toFixed(2)}`);
console.log(formatArr); // ['¥1.00', '¥2.00', '¥3.00']
总结
- 核心区别:
forEach无返回值,用于"执行操作";map返回新数组,用于"数据转换"。 - 次要区别:
map支持链式调用,forEach不支持;两者都无法中断遍历。 - 选型原则:有返回新数组的需求用 map,无则用 forEach,无需纠结性能(日常场景差异可忽略)。