前言
前端里列表渲染、表格处理、统计汇总,几乎都绕不开数组。很多人习惯用 for 循环一把梭,或者 forEach 里改外部变量,写多了既啰嗦又难维护。
用 map / filter / reduce 这三个方法,可以把「遍历 → 筛选 → 聚合」写得更短、更声明式,也更好复用和排查问题。本文用 10 个常见操作,把日常该怎么选、为什么这么选、容易踩的坑讲清楚。
适合读者:
- 会写
JS,但对map/filter/reduce用哪个、什么时候用有点模糊 - 刚学
JS,希望一开始就养成清晰的数组写法 - 有经验的前端,想统一团队里的列表/表格/统计写法
一、先搞清楚:map / filter / reduce在干什么
这三个方法都不是黑魔法,本质是:在不动原数组的前提下,用一次遍历完成「转换 / 筛选 / 汇总」。
| 方法 | 在干什么 | 返回值 |
|---|---|---|
map | 一对一转换,每个元素变成一个新值 | 新数组,长度和原数组相同 |
filter | 按条件筛掉一部分元素 | 新数组,长度 ≤ 原数组 |
reduce | 把整个数组聚合成一个值(或一个对象、数组) | 你定义的「一个结果」 |
// 传统 for:意图分散,还要自己管索引和 push
const names = [];
for (let i = 0; i < users.length; i++) {
names.push(users[i].name);
}
// map:一眼看出「把每条转成 name」
const names = users.map((u) => u.name);
记住一点:能不用索引就不用索引,用 map/filter/reduce 把「要做什么」写清楚,比「怎么循环」更重要。
二、数组的 10 个常用操作
假设接口返回的列表类似:
const list = [
{ id: 1, title: '文章1', status: 'published', readCount: 100 },
{ id: 2, title: '文章2', status: 'draft', readCount: 0 },
{ id: 3, title: '文章3', status: 'published', readCount: 200 },
];
下面 10 个写法,覆盖列表渲染、表格数据、统计汇总等真实场景。
操作 1:列表只取某几个字段(map 做「投影」)
const options = list.map(({ id, title }) => ({ value: id, label: title }));
// [{ value: 1, label: '文章1' }, ...]
适用: 下拉选项、表格列数据、只给前端展示 id + 文案。
注意: 返回的是新对象,不共享引用,不会改到原数组里的对象。
操作 2:按条件筛出一批(filter)
const published = list.filter((item) => item.status === 'published');
适用: 只展示已发布、只算有效订单、去掉空项等。
注意: filter 回调里要返回布尔值,return true 的项才会留下。
操作 3:先筛再转(filter + map)
const publishedTitles = list
.filter((item) => item.status === 'published')
.map((item) => item.title);
适用: 先按状态/类型筛,再只要某几个字段。
推荐: 链式写,先 filter 再 map,读起来就是「先筛后取」,逻辑清晰。
操作 4:求和、求总数(reduce 做汇总)
const totalRead = list.reduce((sum, item) => sum + item.readCount, 0);
适用: 订单总金额、总阅读量、总条数等。
注意: 第二个参数 0 是初始值,必写;否则空数组会报错,且第一轮 sum 会是第一项而不是数字。
操作 5:按某个字段分组(reduce 聚合成对象)
// 1. 定义模拟的原始数据列表(包含不同status的对象)
const list = [
{ id: 1, title: "前端入门教程", status: "published", author: "小明" },
{ id: 2, title: "React实战笔记", status: "draft", author: "小红" },
{ id: 3, title: "Vue3最佳实践", status: "published", author: "小明" },
{ id: 4, title: "Node.js性能优化", status: "archived", author: "小刚" },
{ id: 5, title: "TypeScript进阶", status: "draft", author: "小红" },
];
// 2. 核心逻辑:使用reduce按status字段分组,聚合成对象
const byStatus = list.reduce((acc, item) => {
// 提取当前项的status作为分组的键
const key = item.status;
// 如果累加器中没有该键,先初始化空数组(避免push时报错)
if (!acc[key]) acc[key] = [];
// 将当前项推入对应键的数组中
acc[key].push(item);
// 返回累加器,供下一次迭代使用
return acc;
}, {}); // 初始值必须设为空对象,作为分组结果的容器
// 3. 打印分组结果,验证效果
console.log("按status分组后的结果:");
console.log(byStatus);
代码关键部分解释
- 原始数据
list:模拟了实际业务中常见的对象数组,包含id、title、status、author等字段,status有published(已发布)、draft(草稿)、archived(已归档)三种值,是分组的依据。 reduce回调函数:
acc(累加器):迭代过程中保存分组结果的中间对象,最终会成为最终的分组对象。item:当前迭代到的列表项。key = item.status:提取当前项的status作为分组的 “键”。if (!acc[key]) acc[key] = []:如果acc中没有该键对应的数组,先初始化空数组(否则直接push会报错)。acc[key].push(item):将当前项添加到对应键的数组中。reduce初始值{}:必须显式设置为空对象,否则第一次迭代时acc会是列表的第一个元素,导致分组逻辑出错。
运行结果
执行代码后,控制台会输出如下结构化的分组结果:
按status分组后的结果:
{
published: [
{ id: 1, title: '前端入门教程', status: 'published', author: '小明' },
{ id: 3, title: 'Vue3最佳实践', status: 'published', author: '小明' }
],
draft: [
{ id: 2, title: 'React实战笔记', status: 'draft', author: '小红' },
{ id: 5, title: 'TypeScript进阶', status: 'draft', author: '小红' }
],
archived: [
{ id: 4, title: 'Node.js性能优化', status: 'archived', author: '小刚' }
]
}
适用: 按状态、类型、日期分组,再做不同展示或统计。
注意: 每次都要 return acc,否则下一轮拿到的是 undefined。
操作 6:去重(根据某一字段)
const uniqueByStatus = list.reduce((acc, item) => {
if (!acc.find((x) => x.status === item.status)) acc.push(item);
return acc;
}, []);
// 或简单场景用 Set:[...new Set(list.map((x) => x.status))] 只得到不重复的 status 值
适用: 列表按某字段去重,或先取某字段再 Set 去重。
注意: 对象/数组去重要自己定「什么叫相同」(按 id、按某个key 等)。
操作 7:找「第一个符合条件的」(find + 默认值)
const firstPublished = list.find((item) => item.status === 'published') ?? null;
适用: 默认选中第一项、取第一个有效配置等。
注意: find 找不到返回 undefined,用 ?? 可以统一成 null 或默认对象,避免后面解构报错。
操作 8:是否「全部 / 至少一个」满足条件(every / some)
const allPublished = list.every((item) => item.status === 'published');
const hasDraft = list.some((item) => item.status === 'draft');
适用: 表单校验「是否全部勾选」、权限「是否有任一管理员」等。
注意: 空数组时,every 为 true,some 为 false,业务上要结合「空列表算通过还是不算」处理。
操作 9:将嵌套数组扁平化(拍平一层 / 多层,flat)
const rows = [[1, 2], [3, 4], [5]];
const flatRows = rows.flat(); // 拍平一层,结果:[1, 2, 3, 4, 5]
// 拓展:拍平多层(比如三维数组)
const multiRows = [[1, [2, 3]], [4, [5, [6]]]];
const flatAllRows = multiRows.flat(Infinity); // 拍平所有层级,结果:[1, 2, 3, 4, 5, 6]
适用: 多行多列合并成一行、接口多页结果合并成一维列表。
注意: flat() 默认只拍平一层,flat(2) 或 flat(Infinity) 可拍平多层。
操作 10:先 map 再拍平(flatMap)
const words = ['hello', 'world'];
const letters = words.flatMap((word) => word.split(''));
// ['h','e','l','l','o','w','o','r','l','d']
适用: 每个元素对应一个数组,最后要一维列表(如标签展开、子项打平)。
好处: 不用写 map(...).flat(),一次遍历完成,语义也更清晰。
注意: flatMap 只能拍平一级嵌套,它无法处理两级及以上的多层嵌套结构。。
三、容易踩的坑
1. map 里没 return,得到一数组 undefined
// 错误示例
list.map((item) => {
item.title = item.title.toUpperCase();
});
// 结果:[undefined, undefined, undefined]
核心问题: map 要求返回新元素,代码块里没写 return,默认返回 undefined。
正确写法:
// 要新数组(不改原数据)
const newList = list.map((item) => ({ ...item, title: item.title.toUpperCase() }));
// 要改原数组(用 forEach 更语义化)
list.forEach((item) => {
item.title = item.title.toUpperCase();
});
提醒: map 做「转换出新数组」,forEach 做「遍历改原数据」,别混用。
2. reduce 忘了写初始值或忘了 return
// 错误示例
list.reduce((sum, item) => sum + item.readCount);
// 问题:空数组报错;非空时第一轮 sum 是第一个对象,结果成字符串拼接
核心问题: 初始值缺失导致类型异常,漏 return 会让累加值变成 undefined。
正确写法:
// 求和(初始值 0 + 显式/隐式 return)
const total = list.reduce((sum, item) => sum + item.readCount, 0);
// 分组(初始值 {} + return 累加器)
const group = list.reduce((acc, item) => {
acc[item.status] = acc[item.status] || [];
acc[item.status].push(item);
return acc; // 必须 return
}, {});
提醒: 初始值( 0 / [] / {} )必写,回调里一定要返回累加结果。
3. filter / map 里改了原元素
// 错误示例
list.filter((item) => {
item.visible = item.status === 'published'; // 直接修改原对象
return item.visible;
});
核心问题: filter/map 设计初衷是「只读遍历」,修改原数据易引发隐式 bug。
正确写法:
// 先筛选,再单独修改(逻辑分离)
const publishedList = list.filter((item) => item.status === 'published');
publishedList.forEach((item) => {
item.visible = true;
});
提醒: 筛选 / 转换 和 修改原数据分开写,逻辑更清晰,排查问题更易。
4. 链式太长、中间没有语义
// 错误示例
// 错误示例(可读性差,调试难)
const total = data.map(i => i.readCount).filter(n => n > 0).reduce((s, n) => s + n, 0);
核心问题: 长链式无语义,出问题时无法快速定位哪一步错。 正确写法:
// 拆成有语义的变量,一眼看懂每步做什么
const readCounts = data.map(i => i.readCount); // 提取阅读量
const validCounts = readCounts.filter(n => n > 0); // 过滤有效数据
const total = validCounts.reduce((s, n) => s + n, 0); // 求和
提醒: 链式最多 2-3 步,超过就拆成变量,用变量名说明「这步在做什么」。
踩坑小结
map必写return,改原数组用forEach;reduce初始值和return缺一不可;filter/map不修改原元素,逻辑分离更清晰;- 长链式拆成语义化变量,提升可读性和可调试性。
四、实战推荐写法模板
列表渲染(只取展示用字段):
const { list = [] } = response?.data ?? {};
const options = list.map(({ id, title }) => ({ value: id, label: title }));
表格:先筛再算、再展示:
const rows = (response?.data?.list ?? [])
.filter((item) => item.status !== 'deleted')
.map((item) => ({
...item,
readCount: item.readCount ?? 0,
}));
const total = rows.reduce((sum, r) => sum + r.readCount, 0);
统计汇总(按维度分组 + 汇总):
const byStatus = (response?.data?.list ?? []).reduce((acc, item) => {
const key = item.status ?? 'unknown';
acc[key] = (acc[key] ?? 0) + 1;
return acc;
}, {});
五、小结
| 场景 | 推荐写法 |
|---|---|
| 每个元素变一个新值 | list.map(item => ...) |
| 按条件留一部分 | list.filter(item => ...) |
| 先筛再转 | list.filter(...).map(...) |
| 求和/计数 | list.reduce((sum, x) => sum + x.xxx, 0) |
| 按字段分组 | list.reduce((acc, x) => { ... return acc }, {}) |
| 找第一个满足的 | list.find(...) ?? 默认值 |
| 全部/至少一个 | list.every(...) / list.some(...) |
| 先转再拍平 | list.flatMap(...) |
记住:map 负责「变」,filter 负责「留」,reduce 负责「合」。日常写列表、表格、统计时,先想清楚是变、留还是合,再选方法,代码会干净很多,也少踩坑。
文章到这里结束。如果你日常写列表、表格、统计时经常纠结用哪个方法,希望这篇能帮你定个型。
以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。
我是 Eugene,你的电子学友。
如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~