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

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端正则干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。

前言

前端里列表渲染、表格处理、统计汇总,几乎都绕不开数组。很多人习惯用 for 循环一把梭,或者 forEach 里改外部变量,写多了既啰嗦又难维护。

map / filter / reduce 这三个方法,可以把「遍历 → 筛选 → 聚合」写得更短、更声明式,也更好复用和排查问题。本文用 10 个常见操作,把日常该怎么选、为什么这么选、容易踩的坑讲清楚。

适合读者:

  • 会写 JS,但对 map/filter/reduce 用哪个、什么时候用有点模糊
  • 刚学 JS,希望一开始就养成清晰的数组写法
  • 有经验的前端,想统一团队里的列表/表格/统计写法

一、先搞清楚:map / filter / reduce在干什么

这三个方法都不是黑魔法,本质是:在不动原数组的前提下,用一次遍历完成「转换 / 筛选 / 汇总」

方法 在干什么 返回值
map 一对一转换,每个元素变成一个新值 新数组,长度和原数组相同
filter 按条件筛掉一部分元素 新数组,长度 ≤ 原数组
reduce 把整个数组聚合成一个值(或一个对象、数组) 你定义的「一个结果」
javascript 复制代码
// 传统 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 个常用操作

假设接口返回的列表类似:

javascript 复制代码
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 做「投影」)

javascript 复制代码
const options = list.map(({ id, title }) => ({ value: id, label: title }));
// [{ value: 1, label: '文章1' }, ...]

适用: 下拉选项、表格列数据、只给前端展示 id + 文案。
注意: 返回的是新对象,不共享引用,不会改到原数组里的对象。


操作 2:按条件筛出一批(filter

javascript 复制代码
const published = list.filter((item) => item.status === 'published');

适用: 只展示已发布、只算有效订单、去掉空项等。
注意: filter 回调里要返回布尔值,return true 的项才会留下。


操作 3:先筛再转(filter + map

javascript 复制代码
const publishedTitles = list
  .filter((item) => item.status === 'published')
  .map((item) => item.title);

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


操作 4:求和、求总数(reduce 做汇总)

javascript 复制代码
const totalRead = list.reduce((sum, item) => sum + item.readCount, 0);

适用: 订单总金额、总阅读量、总条数等。
注意: 第二个参数 0 是初始值,必写;否则空数组会报错,且第一轮 sum 会是第一项而不是数字。


操作 5:按某个字段分组(reduce 聚合成对象)

javascript 复制代码
// 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会是列表的第一个元素,导致分组逻辑出错。
运行结果

执行代码后,控制台会输出如下结构化的分组结果:

javascript 复制代码
按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:去重(根据某一字段)

javascript 复制代码
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 + 默认值)

javascript 复制代码
const firstPublished = list.find((item) => item.status === 'published') ?? null;

适用: 默认选中第一项、取第一个有效配置等。
注意: find 找不到返回 undefined,用 ?? 可以统一成 null 或默认对象,避免后面解构报错。


操作 8:是否「全部 / 至少一个」满足条件(every / some

javascript 复制代码
const allPublished = list.every((item) => item.status === 'published');
const hasDraft = list.some((item) => item.status === 'draft');

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


操作 9:将嵌套数组扁平化(拍平一层 / 多层,flat

javascript 复制代码
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

javascript 复制代码
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

javascript 复制代码
// 错误示例
list.map((item) => {
  item.title = item.title.toUpperCase();
});
// 结果:[undefined, undefined, undefined]

核心问题: map 要求返回新元素,代码块里没写 return,默认返回 undefined
正确写法:

javascript 复制代码
// 要新数组(不改原数据)
const newList = list.map((item) => ({ ...item, title: item.title.toUpperCase() }));
// 要改原数组(用 forEach 更语义化)
list.forEach((item) => {
  item.title = item.title.toUpperCase();
});

提醒: map 做「转换出新数组」,forEach 做「遍历改原数据」,别混用。


2. reduce 忘了写初始值或忘了 return

javascript 复制代码
// 错误示例
list.reduce((sum, item) => sum + item.readCount);
// 问题:空数组报错;非空时第一轮 sum 是第一个对象,结果成字符串拼接

核心问题: 初始值缺失导致类型异常,漏 return 会让累加值变成 undefined
正确写法:

javascript 复制代码
// 求和(初始值 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 里改了原元素

javascript 复制代码
// 错误示例
list.filter((item) => {
  item.visible = item.status === 'published'; // 直接修改原对象
  return item.visible;
});

核心问题: filter/map 设计初衷是「只读遍历」,修改原数据易引发隐式 bug
正确写法:

javascript 复制代码
// 先筛选,再单独修改(逻辑分离)
const publishedList = list.filter((item) => item.status === 'published');
publishedList.forEach((item) => {
  item.visible = true;
});

提醒: 筛选 / 转换 和 修改原数据分开写,逻辑更清晰,排查问题更易。


4. 链式太长、中间没有语义

javascript 复制代码
// 错误示例
// 错误示例(可读性差,调试难)
const total = data.map(i => i.readCount).filter(n => n > 0).reduce((s, n) => s + n, 0);

核心问题: 长链式无语义,出问题时无法快速定位哪一步错。
正确写法:

javascript 复制代码
// 拆成有语义的变量,一眼看懂每步做什么
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. 长链式拆成语义化变量,提升可读性和可调试性。

四、实战推荐写法模板

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

javascript 复制代码
const { list = [] } = response?.data ?? {};
const options = list.map(({ id, title }) => ({ value: id, label: title }));

表格:先筛再算、再展示:

javascript 复制代码
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);

统计汇总(按维度分组 + 汇总):

javascript 复制代码
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,你的电子学友,我们下一篇干货见~

相关推荐
晓得迷路了1 小时前
栗子前端技术周刊第 117 期 - TypeScript 6.0 Beta、webpack 2026 年路线图、React 最新生态调查报告结果...
前端·javascript·react.js
摇滚侠1 小时前
bootstrap 框架讲解-快速上手,最适合后端开发人员的bootstrap 保姆级使用教程
前端·bootstrap·html
lzhdim2 小时前
CSS实现毛玻璃模糊效果
前端·css
We་ct2 小时前
浏览器渲染流程(完整+面试背诵版)
前端·面试·职场和发展·edge·edge浏览器
2301_805962932 小时前
从零开始写第一个网页——HTML结构入门教程(小白友好)
前端·html
iambooo2 小时前
Bash 执行机制与进程模型:理解 Shell 的底层逻辑
前端·chrome
a1117762 小时前
个人展示页面(html 线条交互)
前端·开源·html
笨蛋不要掉眼泪2 小时前
Spring Cloud Gateway 核心实战:断言(Predicate)的长短写法与自定义工厂详解
java·前端·微服务·架构
RichardLau_Cx2 小时前
零依赖!纯前端 AI 辅助病例管理系统 aiCaseManage:无后端也能实现诊疗行为核验
前端·人工智能·前端开发·localstorage·医疗科技·ai辅助开发·零依赖项目