数组的 10 个常用操作:map / filter / reduce 实战套路

0 阅读9分钟

前言

前端里列表渲染、表格处理、统计汇总,几乎都绕不开数组。很多人习惯用 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);

适用: 先按状态/类型筛,再只要某几个字段。
推荐: 链式写,先 filtermap,读起来就是「先筛后取」,逻辑清晰。


操作 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);
代码关键部分解释
  1. 原始数据list:模拟了实际业务中常见的对象数组,包含idtitlestatusauthor等字段,statuspublished(已发布)、draft(草稿)、archived(已归档)三种值,是分组的依据。
  2. 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');

适用: 表单校验「是否全部勾选」、权限「是否有任一管理员」等。
注意: 空数组时,everytruesomefalse,业务上要结合「空列表算通过还是不算」处理。


操作 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 步,超过就拆成变量,用变量名说明「这步在做什么」。


踩坑小结

  1. map 必写 return,改原数组用 forEach
  2. reduce 初始值和 return 缺一不可;
  3. filter/map 不修改原元素,逻辑分离更清晰;
  4. 长链式拆成语义化变量,提升可读性和可调试性。

四、实战推荐写法模板

列表渲染(只取展示用字段):

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,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~