当前位置:首页 > 文章列表 > 文章 > 前端 > JavaScriptreduce嵌套数组聚合技巧

JavaScriptreduce嵌套数组聚合技巧

2025-11-28 17:18:39 0浏览 收藏

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

JavaScript 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 函数,将其结果汇总为单个返回值。它接收两个主要参数:

  1. reducer 函数:一个回调函数,包含四个参数:
    • accumulator (累加器):回调函数累计处理的结果。
    • currentValue (当前值):数组中正在处理的当前元素。
    • currentIndex (当前索引,可选):数组中正在处理的当前元素的索引。
    • array (原数组,可选):reduce 被调用的数组。
  2. initialValue (初始值,可选):作为第一次调用 reducer 函数时的 accumulator 值。如果未提供,则 accumulator 将使用数组的第一个元素,并且 currentValue 将从第二个元素开始。在构建复杂结构时,通常强烈建议提供一个合适的 initialValue(例如,空数组 [] 或空对象 {})。

在本场景中,我们将利用 reduce 的累加器来逐步构建目标嵌套结构。

分层聚合逻辑详解

使用 reduce 方法实现上述转换的核心在于,在遍历原始数组的每个元素时,根据 medico、rateio 和 convenio 的值,在累加器中查找或创建对应的嵌套层级,并对 subtotal 进行聚合。

  1. 初始化累加器 我们将 reduce 的初始值设为一个空数组 []。这个空数组将作为最终结果的顶层结构,存储按 medico 分组的对象。

    const result = arr.reduce((acc, obj) => {
      // ... 逻辑 ...
      return acc;
    }, []); // 初始累加器为 []
  2. 第一层分组:按 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,
              },
            ],
          },
        ],
      });
    }
  3. 第二层分组:按 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,
            },
          ],
        });
      }
    }
  4. 第三层分组与聚合:按 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)); // 打印格式化后的结果

注意事项与优化建议

  1. 代码可读性与维护性 虽然 reduce 提供了强大的功能,但多层嵌套的 find 调用可能会降低代码的可读性,尤其是在层级更深的情况下。为了提高可读性,可以考虑将内部的查找和创建逻辑封装成辅助函数。

  2. 性能考量 在上述解决方案中,每次迭代都需要在累加器内部的数组中进行 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 的值转换为数组。

    这将显著提高处理大规模数据的性能。

  3. 数据变异与纯函数 本教程中的 reduce 实现直接修改了累加器 acc 及其内部对象的属性(例如 existingConvenio.sum_subtotal += obj.subtotal)。这种直接修改是 reduce 在构建复杂结构时的一种常见且有效的方法。然而,在强调纯函数式编程和不可变性的场景中,可能需要通过创建新对象(例如使用展开运算符 ...)来避免直接修改原始数据或累加器,但这通常会增加代码的复杂性。对于大多数数据转换场景,本示例中的方法是完全可接受的。

总结

Array.prototype.reduce() 方法是 JavaScript 中一个极其强大的工具,尤其适用于将数组转换或聚合为新的、更复杂的数据结构。通过本教程,我们学习了如何利用 reduce 结合多层查找和条件逻辑,将扁平化的对象数组成功转换为按多级键分组并聚合数值的嵌套结构。理解其工作原理和潜在的性能考量,并根据实际项目需求选择合适的优化策略,将使您能够更高效、更优雅地处理复杂的数据转换任务。

到这里,我们也就讲完了《JavaScriptreduce嵌套数组聚合技巧》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!

Java定时任务教程:ScheduledExecutorService详解Java定时任务教程:ScheduledExecutorService详解
上一篇
Java定时任务教程:ScheduledExecutorService详解
Pythonparam参数详解与使用教程
下一篇
Pythonparam参数详解与使用教程
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3169次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3381次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3410次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4515次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3790次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码