JavaScript reduce 方法实现复杂对象数组的嵌套转换与数据聚合

32次阅读

JavaScript reduce 方法实现复杂对象数组的嵌套转换与数据聚合

本文详细阐述如何利用 javaScript `reduce` 方法将扁平化的对象数组转换成具有多级嵌套结构的数据。通过以 `medico`、`rateio` 和 `convenio` 为键进行分组,并对 `subtotal` 值进行累加,本教程展示了 `reduce` 在复杂数据重塑和聚合场景中的强大功能与实现细节,提供清晰的代码示例和实践建议。

引言:复杂数据转换的需求

前端后端开发中,我们经常会遇到需要将扁平化的数据结构转换为更具层次感的嵌套结构,并在此过程中进行数据聚合的需求。例如,将一系列包含医生、分摊方案和合同信息以及子总额的记录,转换为按医生、分摊方案、合同层层分组并汇总子总额的报表形式。传统上,这可以通过多层循环实现,但 javascriptArray.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 }, ];

我们期望将其转换为以下嵌套聚合结构:

立即学习Java免费学习笔记(深入)”;

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 的累加器来逐步构建目标嵌套结构。

JavaScript reduce 方法实现复杂对象数组的嵌套转换与数据聚合

腾讯智影-AI数字人

基于AI数字人能力,实现7*24小时AI数字人直播带货,低成本实现直播业务快速增增,全天智能在线直播

JavaScript reduce 方法实现复杂对象数组的嵌套转换与数据聚合 73

查看详情 JavaScript 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 结合多层查找和条件逻辑,将扁平化的对象数组成功转换为按多级键分组并聚合数值的嵌套结构。理解其工作原理和潜在的性能考量,并根据实际项目需求选择合适的优化策略,将使您能够更高效、更优雅地处理复杂的数据转换任务。

以上就是JavaScript

text=ZqhQzanResources