JavaScriptreduce嵌套数组聚合技巧
本文深入解析了JavaScript中`reduce`方法在处理嵌套数组数据聚合方面的强大应用。面对前端后端开发中常见的扁平数据结构转换需求,文章以实际案例出发,详细阐述了如何运用`reduce`函数,通过`medico`、`rateio`和`convenio`等多级键进行分组,并对`subtotal`值进行累加,最终生成具有多层嵌套结构的目标数据。教程不仅提供了清晰的代码示例,还深入探讨了`reduce`的核心概念、分层聚合逻辑,以及性能优化策略,如使用Map对象提升查找效率。旨在帮助开发者掌握利用`reduce`高效、优雅地解决复杂数据转换任务的技巧,提升代码可读性和维护性,使其更适用于实际项目开发。

本文详细阐述如何利用 JavaScript `reduce` 方法将扁平化的对象数组转换成具有多级嵌套结构的数据。通过以 `medico`、`rateio` 和 `convenio` 为键进行分组,并对 `subtotal` 值进行累加,本教程展示了 `reduce` 在复杂数据重塑和聚合场景中的强大功能与实现细节,提供清晰的代码示例和实践建议。
引言:复杂数据转换的需求
在前端或后端开发中,我们经常会遇到需要将扁平化的数据结构转换为更具层次感的嵌套结构,并在此过程中进行数据聚合的需求。例如,将一系列包含医生、分摊方案和合同信息以及子总额的记录,转换为按医生、分摊方案、合同层层分组并汇总子总额的报表形式。传统上,这可以通过多层循环实现,但 JavaScript 的 Array.prototype.reduce() 方法提供了一种更函数式、更简洁且通常更高效的解决方案。
考虑以下原始数据结构:
const arr = [
{ medico: "med1", rateio: "rat1", convenio: "conv1", subtotal: 10 },
{ medico: "med2", rateio: "rat2", convenio: "conv2", subtotal: 10 },
{ medico: "med2", rateio: "rat2", convenio: "conv2", subtotal: 20 },
{ medico: "med1", rateio: "rat1", convenio: "conv3", subtotal: 20 },
{ medico: "med1", rateio: "rat1", convenio: "conv3", subtotal: 25 },
{ medico: "med2", rateio: "rat3", convenio: "conv4", subtotal: 15 },
{ medico: "med2", rateio: "rat4", convenio: "conv3", subtotal: 10 },
];我们期望将其转换为以下嵌套聚合结构:
const result = [
{
medico: "med1",
grantotals: [
{
rateio: "rat1",
grandtotals: [
{ convenio: "conv1", sum_subtotal: 10 },
{ convenio: "conv3", sum_subtotal: 45 },
],
},
],
},
{
medico: "med2",
grantotals: [
{
rateio: "rat2",
grandtotals: [{ convenio: "conv2", sum_subtotal: 30 }],
},
{
rateio: "rat3",
grandtotals: [{ convenio: "conv4", sum_subtotal: 15 }],
},
{
rateio: "rat4",
grandtotals: [{ convenio: "conv3", sum_subtotal: 10 }],
},
],
},
];Array.prototype.reduce() 核心概念
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数,将其结果汇总为单个返回值。它接收两个主要参数:
- reducer 函数:一个回调函数,包含四个参数:
- accumulator (累加器):回调函数累计处理的结果。
- currentValue (当前值):数组中正在处理的当前元素。
- currentIndex (当前索引,可选):数组中正在处理的当前元素的索引。
- array (原数组,可选):reduce 被调用的数组。
- initialValue (初始值,可选):作为第一次调用 reducer 函数时的 accumulator 值。如果未提供,则 accumulator 将使用数组的第一个元素,并且 currentValue 将从第二个元素开始。在构建复杂结构时,通常强烈建议提供一个合适的 initialValue(例如,空数组 [] 或空对象 {})。
在本场景中,我们将利用 reduce 的累加器来逐步构建目标嵌套结构。
分层聚合逻辑详解
使用 reduce 方法实现上述转换的核心在于,在遍历原始数组的每个元素时,根据 medico、rateio 和 convenio 的值,在累加器中查找或创建对应的嵌套层级,并对 subtotal 进行聚合。
初始化累加器 我们将 reduce 的初始值设为一个空数组 []。这个空数组将作为最终结果的顶层结构,存储按 medico 分组的对象。
const result = arr.reduce((acc, obj) => { // ... 逻辑 ... return acc; }, []); // 初始累加器为 []第一层分组:按 medico 分组 对于 arr 中的每一个 obj,我们首先在累加器 acc 中查找是否存在 medico 值与 obj.medico 相同的对象。
- 如果找到 existingMedico:说明该医生已存在于结果中,我们继续处理其内部的 grantotals。
- 如果未找到:说明这是一个新的医生,我们需要在 acc 中添加一个新的 medico 对象,并初始化其 grantotals 数组,其中包含当前 obj 对应的 rateio 和 convenio 结构。
const existingMedico = acc.find((item) => item.medico === obj.medico); if (existingMedico) { // 医生已存在,处理 rateio 层级 } else { // 新医生,创建新的 medico 对象及初始结构 acc.push({ medico: obj.medico, grantotals: [ { rateio: obj.rateio, grandtotals: [ { convenio: obj.convenio, sum_subtotal: obj.subtotal, }, ], }, ], }); }第二层分组:按 rateio 分组 如果 medico 已存在 (existingMedico 不为空),我们接着在其 grantotals 数组中查找是否存在 rateio 值与 obj.rateio 相同的对象。
- 如果找到 existingRateio:说明该分摊方案已存在于当前医生的记录中,我们继续处理其内部的 grandtotals。
- 如果未找到:说明这是一个新的分摊方案,我们需要在 existingMedico.grantotals 中添加一个新的 rateio 对象,并初始化其 grandtotals 数组,其中包含当前 obj 对应的 convenio 结构。
if (existingMedico) { const existingRateio = existingMedico.grantotals.find( (item) => item.rateio === obj.rateio ); if (existingRateio) { // 分摊方案已存在,处理 convenio 层级 } else { // 新的分摊方案,创建新的 rateio 对象及初始结构 existingMedico.grantotals.push({ rateio: obj.rateio, grandtotals: [ { convenio: obj.convenio, sum_subtotal: obj.subtotal, }, ], }); } }第三层分组与聚合:按 convenio 分组并累加 subtotal 如果 rateio 也已存在 (existingRateio 不为空),我们接着在其 grandtotals 数组中查找是否存在 convenio 值与 obj.convenio 相同的对象。
- 如果找到 existingConvenio:说明该合同已存在于当前分摊方案中,我们直接将 obj.subtotal 累加到 existingConvenio.sum_subtotal。
- 如果未找到:说明这是一个新的合同,我们需要在 existingRateio.grandtotals 中添加一个新的 convenio 对象,并初始化 sum_subtotal 为 obj.subtotal。
if (existingRateio) { const existingConvenio = existingRateio.grandtotals.find( (item) => item.convenio === obj.convenio ); if (existingConvenio) { // 合同已存在,累加 subtotal existingConvenio.sum_subtotal += obj.subtotal; } else { // 新合同,创建新的 convenio 对象 existingRateio.grandtotals.push({ convenio: obj.convenio, sum_subtotal: obj.subtotal, }); } }
完整代码示例
将上述逻辑整合,形成完整的 reduce 实现:
const arr = [
{ medico: "med1", rateio: "rat1", convenio: "conv1", subtotal: 10 },
{ medico: "med2", rateio: "rat2", convenio: "conv2", subtotal: 10 },
{ medico: "med2", rateio: "rat2", convenio: "conv2", subtotal: 20 },
{ medico: "med1", rateio: "rat1", convenio: "conv3", subtotal: 20 },
{ medico: "med1", rateio: "rat1", convenio: "conv3", subtotal: 25 },
{ medico: "med2", rateio: "rat3", convenio: "conv4", subtotal: 15 },
{ medico: "med2", rateio: "rat4", convenio: "conv3", subtotal: 10 },
];
const result = arr.reduce((acc, obj) => {
// 查找是否存在当前 medico
const existingMedico = acc.find((item) => item.medico === obj.medico);
if (existingMedico) {
// 如果 medico 存在,查找是否存在当前 rateio
const existingRateio = existingMedico.grantotals.find(
(item) => item.rateio === obj.rateio
);
if (existingRateio) {
// 如果 rateio 存在,查找是否存在当前 convenio
const existingConvenio = existingRateio.grandtotals.find(
(item) => item.convenio === obj.convenio
);
if (existingConvenio) {
// 如果 convenio 存在,累加 subtotal
existingConvenio.sum_subtotal += obj.subtotal;
} else {
// 如果 convenio 不存在,添加新的 convenio 对象
existingRateio.grandtotals.push({
convenio: obj.convenio,
sum_subtotal: obj.subtotal,
});
}
} else {
// 如果 rateio 不存在,添加新的 rateio 对象及其初始 convenio
existingMedico.grantotals.push({
rateio: obj.rateio,
grandtotals: [
{
convenio: obj.convenio,
sum_subtotal: obj.subtotal,
},
],
});
}
} else {
// 如果 medico 不存在,添加新的 medico 对象及其初始 rateio 和 convenio
acc.push({
medico: obj.medico,
grantotals: [
{
rateio: obj.rateio,
grandtotals: [
{
convenio: obj.convenio,
sum_subtotal: obj.subtotal,
},
],
},
],
});
}
return acc; // 返回累加器
}, []); // 初始累加器为一个空数组
console.log(JSON.stringify(result, null, 2)); // 打印格式化后的结果注意事项与优化建议
代码可读性与维护性 虽然 reduce 提供了强大的功能,但多层嵌套的 find 调用可能会降低代码的可读性,尤其是在层级更深的情况下。为了提高可读性,可以考虑将内部的查找和创建逻辑封装成辅助函数。
性能考量 在上述解决方案中,每次迭代都需要在累加器内部的数组中进行 find 操作。find 方法的平均时间复杂度为 O(N),其中 N 是被搜索数组的长度。如果原始数组 arr 包含大量数据,并且每个 medico 或 rateio 下的子项也很多,那么这种重复的 find 操作可能导致整体时间复杂度接近 O(N^2),从而影响性能。
优化方案:使用 Map 进行 O(1) 查找 为了提高查找效率,可以使用 JavaScript 的 Map 对象来存储中间结果。Map 提供了 O(1) 的平均时间复杂度进行键值查找。
基本思路是:
- 使用一个 Map 来存储 medico 对象,键为 medico 名称。
- 在每个 medico 对象内部,再使用一个 Map 来存储 rateio 对象,键为 rateio 名称。
- 在每个 rateio 对象内部,再使用一个 Map 来存储 convenio 对象,键为 convenio 名称。
- 最后将 Map 的值转换为数组。
这将显著提高处理大规模数据的性能。
数据变异与纯函数 本教程中的 reduce 实现直接修改了累加器 acc 及其内部对象的属性(例如 existingConvenio.sum_subtotal += obj.subtotal)。这种直接修改是 reduce 在构建复杂结构时的一种常见且有效的方法。然而,在强调纯函数式编程和不可变性的场景中,可能需要通过创建新对象(例如使用展开运算符 ...)来避免直接修改原始数据或累加器,但这通常会增加代码的复杂性。对于大多数数据转换场景,本示例中的方法是完全可接受的。
总结
Array.prototype.reduce() 方法是 JavaScript 中一个极其强大的工具,尤其适用于将数组转换或聚合为新的、更复杂的数据结构。通过本教程,我们学习了如何利用 reduce 结合多层查找和条件逻辑,将扁平化的对象数组成功转换为按多级键分组并聚合数值的嵌套结构。理解其工作原理和潜在的性能考量,并根据实际项目需求选择合适的优化策略,将使您能够更高效、更优雅地处理复杂的数据转换任务。
到这里,我们也就讲完了《JavaScriptreduce嵌套数组聚合技巧》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!
Java定时任务教程:ScheduledExecutorService详解
- 上一篇
- Java定时任务教程:ScheduledExecutorService详解
- 下一篇
- Pythonparam参数详解与使用教程
-
- 文章 · 前端 | 4天前 | 定时器 · 前端 · 性能排查 · 接口请求 · 轮询 · setInterval · setInterval 页面可见性 clearInterval 前端轮询 请求堆积 定时器清理
- 前端轮询接口越打越多怎么办:从重复定时器到清理机制一步步排查
- 490浏览 收藏
-
- 文章 · 前端 | 4天前 | 前端 · 搜索框 · AbortController · 接口请求 · 状态管理 · Fetch AbortController 前端搜索 请求乱序 旧响应覆盖
- 前端搜索结果倒退怎么办:AbortController 取消旧请求和序号兜底
- 295浏览 收藏
-
- 文章 · 前端 | 4天前 | 前端 · 性能优化 · cls · 懒加载 · Core Web Vitals · 前端 图片懒加载 IntersectionObserver CLS 布局稳定
- 前端图片懒加载布局抖动治理完整流程:占位比例、按需加载和 CLS 复查
- 128浏览 收藏
-
- 文章 · 前端 | 5天前 | 工程化 · 前端 · javascript · css · 弹窗 · 前端 z-index 遮罩层 stacking context Portal 弹窗层级
- 前端弹窗层级治理工作流:从 z-index 混乱到 Portal 容器规范
- 350浏览 收藏
-
- 文章 · 前端 | 5天前 | 前端 · javascript · URL参数 · 列表筛选 · 页面状态 · 前端 筛选条件 列表页 history.replaceState URLSearchParams 刷新还原
- 前端筛选条件刷新后丢失怎么办:从内存状态到 URL 参数一步步排查
- 348浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 1125次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 1079次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 1015次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 1206次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 1196次使用
-
- JavaScript函数定义及示例详解
- 2025-05-11 502浏览
-
- 优化用户界面体验的秘密武器:CSS开发项目经验大揭秘
- 2023-11-03 501浏览
-
- 使用微信小程序实现图片轮播特效
- 2023-11-21 501浏览
-
- 解析sessionStorage的存储能力与限制
- 2024-01-11 501浏览
-
- 探索冒泡活动对于团队合作的推动力
- 2024-01-13 501浏览

