0

0

JavaScript递归数组结构转换与父节点数据聚合计算

霞舞

霞舞

发布时间:2025-09-16 10:11:27

|

883人浏览过

|

来源于php中文网

原创

JavaScript递归数组结构转换与父节点数据聚合计算

本文详细阐述如何将具有多层嵌套的JavaScript数组转换为统一的递归树形结构,并着重解决在父节点上聚合其所有子节点数值型数据(如总数和可用量)的挑战。通过分步实现,首先进行结构映射,随后利用后处理机制对父节点数据进行汇总,确保在任意深度层级下都能准确完成数据整合。

1. 问题背景与目标

前端开发中,我们经常需要处理具有复杂嵌套关系的数据结构,例如树形菜单、组织架构或多级分类列表。原始数据可能以扁平化或特定嵌套格式提供,但为了在ui组件(如ant design的tree组件)中展示或进行数据分析,我们需要将其转换为统一的递归树形结构。

本教程的目标是将以下这种包含 group 和 categories、subCategories 的嵌套数组:

const arr = [
  {
    group: { id: "group1", groupname: "groupname1" },
    categories: [
      {
        id: "cat1",
        categoryName: "category1",
        total: 5,
        available: 2,
        subCategories: []
      },
      {
        id: "cat2",
        categoryName: "category2",
        total: 15,
        available: 12,
        subCategories: [
          {
            id: "cat3",
            categoryName: "category3",
            total: 15,
            available: 12,
            subCategories: []
          }
        ]
      }
    ]
  },
  {
    group: { id: "group2", groupname: "groupname2" },
    categories: [
      {
        id: "cat4",
        categoryName: "category4",
        total: 25,
        available: 22,
        subCategories: []
      },
      {
        id: "cat5",
        categoryName: "category5",
        total: 50,
        available: 25,
        subCategories: []
      }
    ]
  }
];

转换为以下统一的树形结构,其中每个节点都包含 key, name, total, available 和 children 属性。特别地,顶层节点(如 group1, group2)的 total 和 available 属性需要聚合其所有子节点的相应值:

[
  {
    "key": "group1",
    "name": "groupname1",
    "total": 35, // 5 + 15 + 15 (cat1.total + cat2.total + cat3.total)
    "available": 26, // 2 + 12 + 12 (cat1.available + cat2.available + cat3.available)
    "children": [
      {
        "key": "cat1",
        "name": "category1",
        "total": 5,
        "available": 2,
        "children": []
      },
      {
        "key": "cat2",
        "name": "category2",
        "total": 30, // 15 + 15 (cat2.total + cat3.total)
        "available": 24, // 12 + 12 (cat2.available + cat3.available)
        "children": [
          {
            "key": "cat3",
            "name": "category3",
            "total": 15,
            "available": 12,
            "children": []
          }
        ]
      }
    ]
  },
  // ... 其他组
]

(注:根据原始问题,group1 的 total 和 available 应该聚合其直接子节点及其孙子节点的所有值。例如,cat2 的 total 应为 15 + 15 = 30,group1 的 total 应为 5 + 30 = 35。原始问题中的期望输出 total: 20, available: 24 可能是一个笔误,或者只考虑了直接子节点。本教程将实现聚合所有子孙节点的功能。)

2. 第一步:构建递归树形结构

首先,我们需要一个递归函数来遍历原始数组,并将其中的 group、category 和 subCategory 对象转换为目标格式的节点。这个函数将负责映射 id 到 key,groupname/categoryName 到 name,并处理 categories/subCategories 到 children 的转换。

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

const formatter = (data) => {
  // 递归处理单个项目,将其转换为目标结构
  const recursiveTree = (item) => {
    if (item.group) { // 处理顶层group项目
      const {
        group: { id, groupname }, // 提取group的id和名称
        categories
      } = item;
      return {
        key: id,
        name: groupname,
        total: 0,      // 初始设置为0,待后续聚合
        available: 0,  // 初始设置为0,待后续聚合
        children: categories?.map(recursiveTree) || [] // 递归处理子分类
      };
    } else { // 处理category或subCategory项目
      const { id, categoryName, total, available, subCategories } = item;
      // 对于category/subCategory,其自身的total和available是已知的
      const children = subCategories?.map(recursiveTree) || [];

      // 如果category/subCategory有子节点,其total和available也需要聚合
      const aggregatedTotal = children.reduce((sum, child) => sum + child.total, total || 0);
      const aggregatedAvailable = children.reduce((sum, child) => sum + child.available, available || 0);

      return {
        key: id,
        name: categoryName,
        total: aggregatedTotal,
        available: aggregatedAvailable,
        children: children
      };
    }
  };

  // 遍历原始数据数组,应用递归转换
  return data.map(recursiveTree);
};

代码解析:

  • formatter 函数接收原始数组 data。
  • recursiveTree 是一个内部递归函数,它根据传入 item 的结构来判断是 group 还是 category/subCategory。
  • 对于 group 类型:
    • 它提取 group.id 和 group.groupname 分别作为 key 和 name。
    • total 和 available 暂时初始化为 0,因为它们最终需要从其所有子孙节点聚合而来。
    • children 属性通过递归调用 categories.map(recursiveTree) 来生成。
  • 对于 category 或 subCategory 类型:
    • 它提取 id、categoryName、total、available。
    • children 属性通过递归调用 subCategories.map(recursiveTree) 来生成。
    • 关键点: 对于 category 或 subCategory 自身,如果它也有子节点,那么它的 total 和 available 也应该聚合自身的值和其所有子孙节点的值。这里在返回之前进行了 reduce 操作,将子节点的 total 和 available 累加到当前节点的初始值上。

3. 第二步:实现顶层父节点数据的聚合计算

上述 recursiveTree 函数已经能够处理 category 和 subCategory 自身的聚合,但对于最顶层的 group 节点,由于其 total 和 available 是从其 categories 聚合而来,而 categories 的聚合又依赖于其 subCategories,这种“自下而上”的计算最好在所有子节点都处理完毕后进行。

PhotoScissors
PhotoScissors

免费自动图片背景去除

下载

一种高效的方法是在 formatter 函数的最后,对已经完成结构转换的顶层节点进行一次后处理。

const formatterWithAggregation = (data) => {
  // 递归处理单个项目,将其转换为目标结构并计算自身及子孙节点的聚合值
  const recursiveTree = (item) => {
    if (item.group) { // 处理顶层group项目
      const {
        group: { id, groupname },
        categories
      } = item;
      // 递归处理所有子分类
      const children = categories?.map(recursiveTree) || [];

      // 对于group节点,其total和available是所有子孙节点的聚合
      const aggregatedTotal = children.reduce((sum, child) => sum + child.total, 0);
      const aggregatedAvailable = children.reduce((sum, child) => sum + child.available, 0);

      return {
        key: id,
        name: groupname,
        total: aggregatedTotal,
        available: aggregatedAvailable,
        children: children
      };
    } else { // 处理category或subCategory项目
      const { id, categoryName, total, available, subCategories } = item;
      const children = subCategories?.map(recursiveTree) || [];

      // 对于category/subCategory,其自身的total和available是自身值与子孙值的聚合
      const aggregatedTotal = children.reduce((sum, child) => sum + child.total, total || 0);
      const aggregatedAvailable = children.reduce((sum, child) => sum + child.available, available || 0);

      return {
        key: id,
        name: categoryName,
        total: aggregatedTotal,
        available: aggregatedAvailable,
        children: children
      };
    }
  };

  // 直接在map过程中完成所有层级的聚合计算
  return data.map(recursiveTree);
};

优化说明:

在第一次的代码实现中,group 节点的 total 和 available 被初始化为 0,而 category 节点的 total 和 available 则在递归返回时进行了聚合。实际上,我们可以将聚合逻辑统一到 recursiveTree 函数内部,在每次递归返回时,都确保当前节点的 total 和 available 已经包含了其所有子孙节点的聚合值。这样,当 group 节点处理其 children (即 categories) 并得到它们已经聚合好的 total 和 available 后,就可以直接进行求和。

这种“自底向上”的递归聚合方法更为简洁和高效,因为它避免了额外的后处理循环。

4. 完整代码示例

将上述逻辑整合,得到最终的解决方案:

const arr = [
  {
    group: { id: "group1", groupname: "groupname1" },
    categories: [
      {
        id: "cat1",
        categoryName: "category1",
        total: 5,
        available: 2,
        subCategories: []
      },
      {
        id: "cat2",
        categoryName: "category2",
        total: 15,
        available: 12,
        subCategories: [
          {
            id: "cat3",
            categoryName: "category3",
            total: 15,
            available: 12,
            subCategories: []
          }
        ]
      }
    ]
  },
  {
    group: { id: "group2", groupname: "groupname2" },
    categories: [
      {
        id: "cat4",
        categoryName: "category4",
        total: 25,
        available: 22,
        subCategories: []
      },
      {
        id: "cat5",
        categoryName: "category5",
        total: 50,
        available: 25,
        subCategories: []
      }
    ]
  }
];

const formatter = (data) => {
  // 递归处理单个项目,将其转换为目标结构并计算自身及子孙节点的聚合值
  const recursiveTree = (item) => {
    if (item.group) { // 处理顶层group项目
      const {
        group: { id, groupname },
        categories
      } = item;
      // 递归处理所有子分类
      const children = categories?.map(recursiveTree) || [];

      // 对于group节点,其total和available是所有子孙节点的聚合
      const aggregatedTotal = children.reduce((sum, child) => sum + child.total, 0);
      const aggregatedAvailable = children.reduce((sum, child) => sum + child.available, 0);

      return {
        key: id,
        name: groupname,
        total: aggregatedTotal,
        available: aggregatedAvailable,
        children: children
      };
    } else { // 处理category或subCategory项目
      const { id, categoryName, total, available, subCategories } = item;
      const children = subCategories?.map(recursiveTree) || [];

      // 对于category/subCategory,其自身的total和available是自身值与子孙值的聚合
      // 注意:这里total和available的初始值是当前item自身的total/available
      const aggregatedTotal = children.reduce((sum, child) => sum + child.total, total || 0);
      const aggregatedAvailable = children.reduce((sum, child) => sum + child.available, available || 0);

      return {
        key: id,
        name: categoryName,
        total: aggregatedTotal,
        available: aggregatedAvailable,
        children: children
      };
    }
  };

  // 直接在map过程中完成所有层级的聚合计算
  return data.map(recursiveTree);
};

const result = formatter(arr);
console.log(JSON.stringify(result, null, 2));

5. 注意事项与扩展

  1. N层深度支持: 这种递归方法天然支持任意深度的嵌套,无需额外修改。
  2. 默认值处理: 在聚合计算中,total || 0 和 available || 0 的写法确保了即使原始数据中缺少 total 或 available 字段,也能将其视为 0 进行计算,避免了 NaN 的出现。
  3. 性能考量: 对于非常庞大和深层的数据结构,递归可能会导致栈溢出。在JavaScript中,通常浏览器对递归深度有限制(约几千层)。如果遇到此类问题,可以考虑使用迭代方式(如队列或栈)模拟递归,或者进行尾递归优化(如果环境支持)。但对于大多数常见应用场景,此递归方案足够高效。
  4. 聚合逻辑扩展: 如果需要聚合的不仅仅是 total 和 available,或者需要不同的聚合方式(如平均值、最大值),只需在 reduce 回调函数中调整相应的逻辑即可。
  5. 错误处理: 在实际应用中,可能需要增加对输入数据格式的校验,例如检查 group、categories、subCategories 等属性是否存在,以增强代码的健壮性。

总结

本教程通过一个两阶段(但最终优化为一步到位)的递归处理方法,成功地将复杂的嵌套数组结构转换为统一的树形结构,并实现了父节点对其所有子孙节点数值型数据的聚合计算。核心在于利用递归的“自底向上”特性,在每次递归调用返回时,确保当前节点的聚合值已经计算完毕,从而使得上层节点可以直接使用这些聚合值进行进一步的汇总。这种模式在处理各种树形数据结构转换和数据汇总的场景中具有广泛的应用价值。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

559

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

437

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

776

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

479

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

554

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1091

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

659

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

554

2023.09.20

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

58

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 4.1万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号