一句话概括:forEach、map、filter、reduce、some、every 是 JavaScript 中最常用的数组遍历方法,它们在返回值、副作用和使用场景上各有差异,面试中常被要求对比分析。
背景
在日常前端开发中,数组遍历是最常见的操作之一。JavaScript 提供了丰富的数组遍历方法,它们看似相似,但实际上在返回值、是否改变原数组、性能特点等方面有显著差异。掌握这些方法的区别,不仅是编写高质量代码的基础,也是前端面试中的高频考点。
本文将系统对比 forEach、map、filter、reduce、some、every 这六个核心方法,通过代码示例和底层原理分析,帮助你彻底掌握它们的适用场景。
概念与定义
1. forEach 方法
1
| array.forEach(callback(currentValue, index, array), thisArg)
|
- 返回值:始终返回
undefined - 是否改变原数组:取决于回调函数是否修改
- 特点:最基础的遍历方法,无法 break 或 return
2. map 方法
1
| array.map(callback(currentValue, index, array), thisArg)
|
- 返回值:返回由回调函数返回值组成的新数组
- 是否改变原数组:回调函数内部不影响原数组
- 特点:用于转换数据,一一映射
3. filter 方法
1
| array.filter(callback(element, index, array), thisArg)
|
- 返回值:返回满足条件的元素组成的新数组
- 是否改变原数组:不影响原数组
- 特点:用于筛选数据
4. reduce 方法
1
| array.reduce(callback(accumulator, currentValue, index, array), initialValue)
|
- 返回值:返回累计计算的结果
- 是否改变原数组:不影响原数组
- 特点:最强大,可实现任意累计逻辑
5. some 方法
1
| array.some(callback(element, index, array), thisArg)
|
- 返回值:返回布尔值(是否有元素满足条件)
- 是否改变原数组:不影响原数组
- 特点:短路求值,找到第一个满足条件的元素就停止
6. every 方法
1
| array.every(callback(element, index, array), thisArg)
|
- 返回值:返回布尔值(是否所有元素都满足条件)
- 是否改变原数组:不影响原数组
- 特点:短路求值,遇到不满足条件的元素就停止
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| const arr = [1, 2, 3, 4, 5];
// forEach: 返回 undefined
const forEachResult = arr.forEach(item => console.log(item));
console.log(forEachResult); // undefined
// map: 返回新数组 [2, 4, 6, 8, 10]
const mapResult = arr.map(item => item * 2);
// filter: 返回新数组 [2, 4]
const filterResult = arr.filter(item => item % 2 === 0);
// reduce: 返回累加结果 15
const reduceResult = arr.reduce((acc, cur) => acc + cur, 0);
// some: 返回 true(存在大于3的元素)
const someResult = arr.some(item => item > 3);
// every: 返回 false(不是所有元素都大于3)
const everyResult = arr.every(item => item > 3);
|
核心知识点拆解
返回值对比
| 方法 | 返回值 | 用途 |
|---|
| forEach | undefined | 仅遍历,无返回值需求 |
| map | 新数组 | 数据转换 |
| filter | 新数组 | 数据筛选 |
| reduce | 任意值 | 累计计算 |
| some | boolean | 判断是否存在 |
| every | boolean | 判断是否全部满足 |
能否中断遍历
- forEach:无法 break 或 return false 终止
- some:找到第一个 true 后立即停止
- every:找到第一个 false 后立即停止
- map/filter/reduce:始终遍历完整个数组
性能对比
在大多数情况下,这几个方法的性能差异不大。但在极端大数据量场景下:
- forEach 略快于 map(因为不需要创建新数组)
- some/every 可能更快(因为短路特性)
常见误区
- map 不等于 forEach:map 会创建新数组,forEach 返回 undefined
- 不要在 map 中做副作用:应该用 forEach
- reduce 可以替代其他方法:所有遍历方法都可以用 reduce 实现
实战案例
案例一:数据转换
1
2
3
4
5
6
7
| // 将用户数组转换为 ID 数组
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
const ids = users.map(user => user.id); // [1, 2]
|
案例二:数据筛选
1
2
3
4
5
6
7
8
9
| // 过滤出成年人
const people = [
{ name: 'Alice', age: 17 },
{ name: 'Bob', age: 25 },
{ name: 'Charlie', age: 16 }
];
const adults = people.filter(person => person.age >= 18);
// [{ name: 'Bob', age: 25 }]
|
案例三:累计计算
1
2
3
4
5
6
7
| // 计算购物车总价
const cart = [
{ name: 'iPhone', price: 7999 },
{ name: 'AirPods', price: 1299 }
];
const total = cart.reduce((sum, item) => sum + item.price, 0); // 9298
|
案例四:条件判断
1
2
3
4
5
6
| // 检查是否有权限
const permissions = ['read', 'write', 'delete'];
const required = 'admin';
const hasPermission = permissions.some(p => p === required); // false
const allValid = permissions.every(p => typeof p === 'string'); // true
|
案例五:reduce 实现 map 和 filter
1
2
3
4
5
6
7
| // 用 reduce 实现 map
const mapViaReduce = (arr, fn) =>
arr.reduce((acc, cur, i) => [...acc, fn(cur, i)], []);
// 用 reduce 实现 filter
const filterViaReduce = (arr, fn) =>
arr.reduce((acc, cur, i) => fn(cur, i) ? [...acc, cur] : acc, []);
|
底层原理
forEach 底层实现
1
2
3
4
5
6
7
8
9
10
11
12
| Array.prototype.forEach = function(callback, thisArg) {
// 1. this 转为对象
const O = Object(this);
// 2. 获取长度
const len = O.length >>> 0;
// 3. 遍历调用
for (let i = 0; i < len; i++) {
if (i in O) {
callback.call(thisArg, O[i], i, O);
}
}
};
|
关键点:
i in O 检查索引是否存在(跳过稀疏数组的空位)- 使用
call 确保回调函数中的 this 正确 - 无返回值,始终返回 undefined
map 底层实现
1
2
3
4
5
6
7
8
9
10
11
12
| Array.prototype.map = function(callback, thisArg) {
const O = Object(this);
const len = O.length >>> 0;
const result = new Array(len); // 创建新数组
for (let i = 0; i < len; i++) {
if (i in O) {
result[i] = callback.call(thisArg, O[i], i, O);
}
}
return result;
};
|
与 forEach 的区别:
- 预先创建了与原数组等长的新数组
- 将回调返回值存入新数组
- 返回新数组引用
reduce 底层实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| Array.prototype.reduce = function(callback, initialValue) {
const O = Object(this);
const len = O.length >>> 0;
let accumulator, startIndex;
// 确定初始值
if (arguments.length > 1) {
accumulator = initialValue;
startIndex = 0;
} else {
// 找到第一个非空索引作为初始值
let k = 0;
while (k < len && !(k in O)) k++;
if (k === len) throw new TypeError('Reduce of empty array');
accumulator = O[k++];
}
for (let i = k; i < len; i++) {
if (i in O) {
accumulator = callback(accumulator, O[i], i, O);
}
}
return accumulator;
};
|
高频面试题解析
Q1: map 和 forEach 的区别?
答案:
- 返回值:map 返回新数组,forEach 返回 undefined
- 用途:map 用于数据转换,forEach 用于仅遍历
- 性能:forEach 略快(无需创建新数组)
Q2: 如何在遍历中中断?
答案:
- 使用 some 或 every 短路特性
- 使用 for…of 配合 break
- 使用 for 循环
- forEach 和 map 无法中断
Q3: reduce 的初始值什么时候需要?
答案:
- 数组为空时:必须提供初始值,否则报错
- 累计结果类型与元素类型不同时:如求最大值、对象累加
- 逻辑需要”空状态”时:如累加对象
Q4: 能否用 filter 实现去重?
答案:可以,但需要结合 indexOf 或 Map:
1
2
3
4
5
| // 简单去重
const unique = arr.filter((item, index) => arr.indexOf(item) === index);
// Map 去重(推荐)
const unique = [...new Map(arr.map(item => [item, item])).values()];
|
总结与扩展
方法选择指南
| 场景 | 推荐方法 |
|---|
| 仅遍历,无返回值 | forEach |
| 数据转换(一对一) | map |
| 数据筛选 | filter |
| 累计计算(求和、分类) | reduce |
| 判断是否存在 | some |
| 判断是否全部满足 | every |
扩展阅读
- find / findIndex:返回第一个满足条件的元素/索引
- flatMap:先 map 再 flat,扁平化映射
- sort:排序(会改变原数组)
- entries / keys / values:遍历键值对/键/值
性能优化建议
- 优先使用原生方法(V8 做了大量优化)
- 大数据量场景考虑使用 for 循环
- 避免在遍历中修改数组
- 稀疏数组使用 for…in 或 Object.keys
参考资料:MDN Array.prototype、ECMAScript 规范、V8 引擎源码