map、filter、reduce 有什么区别?使用时要注意什么?
简化版
map 用来一对一转换数组元素,返回等长新数组;filter 用来筛选元素,返回满足条件的新数组;reduce 用来把数组累积成一个值,可以是数字、对象、数组等。
详细版
三者都不会直接改变原数组,但回调里修改对象元素仍可能产生副作用。
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
[1, 2, 3].filter(x => x > 1); // [2, 3]
[1, 2, 3].reduce((s, x) => s + x, 0); // 6
面试重点:
map关注转换。filter关注保留或丢弃。reduce关注累积。reduce最好提供初始值,避免空数组报错和类型推断混乱。
完整版教学
一、三者都是数组迭代抽象
map/filter/reduce 都会按顺序遍历数组元素。
它们的区别不在于“能不能循环”,而在于表达的意图不同。
好的代码会让读者一眼看出你想转换、筛选还是汇总。
const nums = [1, 2, 3, 4];
如果目标是把每个数翻倍,用 map。
如果目标是留下偶数,用 filter。
如果目标是求总和,用 reduce。
同样是遍历 4 个元素,语义却完全不同。
二、map 是一对一转换
map 对每个元素执行回调,并把每次回调返回值组成新数组。
输入数组有几个元素,输出数组通常就有几个元素。
它适合数据结构转换、字段提取、列表渲染前加工。
const users = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' }
];
const names = users.map(user => user.name);
// ['A', 'B']
数字例子:长度为 2 的 users,map 后 names 长度仍是 2。
如果你的回调里只做副作用却没有使用返回值,通常不该用 map,应该考虑 forEach。
三、filter 是条件筛选
filter 回调返回真值时保留元素,返回假值时丢弃元素。
它不改变元素本身,只决定元素是否进入新数组。
输出数组长度可能小于或等于输入数组长度。
const nums = [1, 2, 3, 4, 5];
const even = nums.filter(n => n % 2 === 0);
// [2, 4]
这里输入 5 个元素,输出 2 个元素。 面试里常问“filter 是否会改变原数组”,答案是不会改变数组结构。 但如果数组元素是对象,回调中修改对象属性仍会影响共享对象。
四、reduce 是把多个元素折叠成一个结果
reduce 接收累积器和当前元素。
它可以求和,也可以分组、构建 Map、拍平数组、统计频次。
关键是初始值决定累积器的类型和第一次迭代行为。
const sum = [1, 2, 3].reduce((acc, cur) => acc + cur, 0);
// 6
const count = ['a', 'b', 'a'].reduce((acc, cur) => {
acc[cur] = (acc[cur] ?? 0) + 1;
return acc;
}, {});
// { a: 2, b: 1 }
流程如下:
初始 acc = 0
读 1 -> acc = 1
读 2 -> acc = 3
读 3 -> acc = 6
reduce 很强,但过度使用会让简单逻辑变难读。
五、reduce 为什么建议总是给初始值
如果不给初始值,reduce 会把数组第一个元素当作初始累积器,并从第二个元素开始遍历。
空数组没有第一个元素,因此会报错。
给初始值可以让行为稳定,也让累积器类型更清楚。
[].reduce((a, b) => a + b); // TypeError
[].reduce((a, b) => a + b, 0); // 0
| 写法 | 空数组 | 累积器初始类型 |
|---|---|---|
| 无初始值 | 报错 | 第一个元素类型 |
| 有初始值 | 返回初始值 | 初始值类型 |
在 TypeScript 项目里,初始值还能帮助类型推断。
六、副作用和性能要看场景
map/filter/reduce 返回新数组或新结果,不直接修改原数组。
但它们不是自动不可变保护。
如果元素是对象,回调里修改对象会影响原数组里的同一个对象。
const list = [{ count: 1 }];
const next = list.map(item => {
item.count += 1;
return item;
});
console.log(list[0].count); // 2
记忆钩子:数组方法不改“数组壳”,不代表不会改“对象芯”。
性能方面,链式调用会产生中间数组。 小中型列表可读性优先,超大数据或热点逻辑再考虑一次循环完成。
七、常见误区与追问
- 误区:
map可以用来纯做遍历副作用。 如果不用返回的新数组,语义上更适合forEach或普通循环。 - 误区:这些方法一定不会产生副作用。 方法本身不改原数组结构,但回调里仍可能修改对象元素。
- 误区:
reduce永远比循环高级。 复杂累积逻辑用 reduce 可能降低可读性,清晰更重要。 - 追问:
reduce不传初始值有什么风险? 空数组会报错,累积器类型也可能和预期不同。 - 追问:
filter(Boolean)有什么作用? 它会过滤掉所有假值,包括0、空字符串、false、null、undefined。 - 追问:链式
filter().map()有什么成本? 会产生中间数组,数据量很大时可能需要优化。
八、加强记忆
把三者按动词记:map 转换,filter 筛选,reduce 累积。答题时用 [1,2,3] 的结果举例,再强调 reduce 初始值和对象元素副作用,基本就能覆盖常见追问。