forEach 和 map 方法区别详解
快速对比表
| 特性 | forEach | map |
|---|---|---|
| 返回值 | undefined |
返回新数组 |
| 是否改变原数组 | 可能(如果在回调中修改) | 不改变原数组 |
| 链式调用 | 不支持(返回undefined) | 支持 |
| 性能 | 稍快(不创建新数组) | 稍慢(创建新数组) |
| 使用场景 | 执行副作用 | 数据转换/映射 |
| 中断循环 | 无法中断 | 无法中断 |
| 空数组处理 | 跳过空元素 | 跳过空元素 |
核心区别
1. 返回值不同(最重要区别)
js
const numbers = [1, 2, 3, 4, 5];
// forEach - 返回 undefined
const forEachResult = numbers.forEach(num => num * 2);
console.log(forEachResult); // undefined
// map - 返回新数组
const mapResult = numbers.map(num => num * 2);
console.log(mapResult); // [2, 4, 6, 8, 10]
2. 使用场景不同
js
// 场景1:执行副作用(适合用 forEach)
const users = [
{ id: 1, name: 'Alice', active: false },
{ id: 2, name: 'Bob', active: true }
];
// 修改原数组元素 - forEach
users.forEach(user => {
user.active = true; // 直接修改原对象
});
console.log(users); // 原数组被修改
// 场景2:创建新数组(适合用 map)
const numbers = [1, 2, 3];
// 创建新数组 - map
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // [1, 4, 9]
console.log(numbers); // [1, 2, 3] 原数组不变
详细示例对比
示例1:基本使用
js
const arr = [1, 2, 3, 4];
// forEach - 遍历执行操作
arr.forEach((item, index) => {
console.log(`索引 ${index}: ${item}`);
});
// 输出:
// 索引 0: 1
// 索引 1: 2
// 索引 2: 3
// 索引 3: 4
// map - 转换数组
const doubled = arr.map(item => item * 2);
console.log(doubled); // [2, 4, 6, 8]
示例2:实际应用场景
js
const products = [
{ id: 1, name: 'Laptop', price: 1000 },
{ id: 2, name: 'Phone', price: 500 },
{ id: 3, name: 'Tablet', price: 300 }
];
// forEach 使用场景:计算总价,更新DOM,日志记录等
let totalPrice = 0;
products.forEach(product => {
totalPrice += product.price;
// 模拟更新UI
console.log(`已处理: ${product.name}`);
});
console.log(`总价: ${totalPrice}`);
// map 使用场景:提取数据,转换格式
const productNames = products.map(product => product.name);
console.log(productNames); // ['Laptop', 'Phone', 'Tablet']
const discountedProducts = products.map(product => ({
...product,
price: product.price * 0.9, // 打9折
discount: '10% off'
}));
console.log(discountedProducts);
// 原products数组不变
示例3:链式调用
js
const numbers = [1, 2, 3, 4, 5, 6];
// map 支持链式调用
const result = numbers
.filter(num => num % 2 === 0) // [2, 4, 6]
.map(num => num * 10) // [20, 40, 60]
.reduce((sum, num) => sum + num, 0); // 120
console.log(result); // 120
// forEach 不支持链式调用(因为返回undefined)
// 下面的代码会报错
// numbers.forEach(num => num * 2).filter(num => num > 5); // ❌
示例4:性能对比
js
// 创建大型数组
const largeArray = Array.from({ length: 1000000 }, (_, i) => i + 1);
// 测试forEach性能
console.time('forEach');
let sum1 = 0;
largeArray.forEach(num => {
sum1 += num;
});
console.timeEnd('forEach');
// 测试map性能
console.time('map');
let sum2 = 0;
largeArray.map(num => {
sum2 += num; // 注意:这里用map做累加是错误用法
return num;
});
console.timeEnd('map');
// forEach通常比map快约10-20%,因为map需要创建新数组
特殊行为和陷阱
1. 处理稀疏数组
js
const sparseArray = [1, , 3, , 5]; // 有空元素
console.log('forEach:');
sparseArray.forEach((item, index) => {
console.log(`索引 ${index}: ${item}`);
});
// 输出:
// 索引 0: 1
// 索引 2: 3
// 索引 4: 5
// 跳过了空元素
console.log('map:');
const mapped = sparseArray.map((item, index) => {
console.log(`处理索引 ${index}: ${item}`);
return item ? item * 2 : 0;
});
console.log(mapped); // [2, 空, 6, 空, 10]
// 注意:空元素位置在map中仍然存在
2. 无法中断循环
js
const numbers = [1, 2, 3, 4, 5];
// 使用 forEach - 无法中断
numbers.forEach(num => {
console.log(num);
if (num === 3) {
return; // 这只是退出当前回调,循环继续
}
});
// 输出: 1 2 3 4 5
// 使用 map - 同样无法中断
numbers.map(num => {
console.log(num);
if (num === 3) {
return 0; // 继续执行所有元素
}
return num;
});
// 如果需要中断,使用 for...of 或 for 循环
for (const num of numbers) {
console.log(num);
if (num === 3) {
break; // 可以中断
}
}
3. 异步处理
js
// 两者处理异步的方式相同
const urls = ['/api/data1', '/api/data2', '/api/data3'];
// 使用 forEach - 无法按顺序处理异步
async function processWithForEach() {
urls.forEach(async (url) => {
const response = await fetch(url);
const data = await response.json();
console.log(data);
});
console.log('forEach 完成'); // 会先执行
}
// 使用 map - 同样无法顺序执行
async function processWithMap() {
const promises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
const results = await Promise.all(promises);
console.log(results);
}
最佳实践建议
使用 forEach 当:
js
// 1. 执行副作用(修改外部变量、DOM操作、日志等)
const items = ['item1', 'item2', 'item3'];
const listElement = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
listElement.appendChild(li); // DOM操作
});
// 2. 遍历对象数组并修改
const users = [{ name: 'Alice' }, { name: 'Bob' }];
users.forEach(user => {
user.active = true; // 修改原对象
});
// 3. 不需要返回值的简单遍历
[1, 2, 3, 4, 5].forEach(num => {
console.log(`数字: ${num}`);
});
使用 map 当:
js
// 1. 数据转换
const prices = [100, 200, 300];
const pricesWithTax = prices.map(price => ({
original: price,
withTax: price * 1.1
}));
// 2. 提取特定属性
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
const userNames = users.map(user => user.name);
// 3. 链式操作的一部分
const numbers = [1, 2, 3, 4, 5];
const evenSquares = numbers
.filter(n => n % 2 === 0)
.map(n => n * n); // [4, 16]
// 4. 需要保持原始数组不变
const original = [1, 2, 3];
const processed = original.map(n => n * 2);
// original 保持不变
性能优化建议
js
// 不好的做法:使用 map 但忽略返回值
const arr = [1, 2, 3, 4, 5];
// ❌ 错误:用map但忽略返回值(应该用forEach)
arr.map(num => {
console.log(num); // 副作用
// 没有返回值,新数组会是 [undefined, undefined, ...]
});
// ✅ 正确:用forEach执行副作用
arr.forEach(num => {
console.log(num);
});
// ✅ 正确:用map转换数据
const doubled = arr.map(num => num * 2);
// 对于大型数组,如果不需要新数组,用forEach
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);
// 只需要累加时
let sum = 0;
largeArray.forEach(num => {
sum += num;
});
// 需要新数组时
const incremented = largeArray.map(num => num + 1);
总结
核心记忆点:
- forEach:用于"做事情"(执行副作用),不关心返回值
- map:用于"转换数据",关心返回值并创建新数组
简单判断法则:
- 如果需要在遍历中修改外部状态或执行操作 → 用 forEach
- 如果需要基于原数组创建新数组 → 用 map
- 如果需要链式调用 → 用 map
- 如果函数有返回值但不需要新数组 → 可能用错方法了