使用JavaScript将扁平数据转换为树形结构的多种实现方法

 更新时间:2025年04月30日 08:41:35   作者:北辰alk  
在实际开发中,我们经常需要将扁平的数据结构(如数据库查询结果)转换为树形结构(如菜单、评论回复等),本文将详细介绍多种实现方法,并提供完整的代码示例,需要的朋友可以参考下

一、理解数据结构

1. 扁平数据结构示例

const flatData = [
  { id: 1, name: '部门1', parentId: 0 },
  { id: 2, name: '部门2', parentId: 1 },
  { id: 3, name: '部门3', parentId: 1 },
  { id: 4, name: '部门4', parentId: 3 },
  { id: 5, name: '部门5', parentId: 3 },
  { id: 6, name: '部门6', parentId: 0 },
];

2. 期望的树形结构

[
  {
    id: 1,
    name: '部门1',
    children: [
      {
        id: 2,
        name: '部门2'
      },
      {
        id: 3,
        name: '部门3',
        children: [
          { id: 4, name: '部门4' },
          { id: 5, name: '部门5' }
        ]
      }
    ]
  },
  {
    id: 6,
    name: '部门6'
  }
]

二、实现方法

方法1:递归实现(经典算法)

function buildTree(items, parentId = 0) {
  const result = [];
  
  for (const item of items) {
    if (item.parentId === parentId) {
      const children = buildTree(items, item.id);
      if (children.length) {
        item.children = children;
      }
      result.push(item);
    }
  }
  
  return result;
}

const treeData = buildTree(flatData);
console.log(JSON.stringify(treeData, null, 2));

优点

  • 逻辑清晰直观
  • 适合理解树形结构的构建过程

缺点

  • 时间复杂度较高(O(n^2))
  • 大数据量时性能较差

方法2:使用对象引用(高效算法)

function buildTreeOptimized(items) {
  const itemMap = {};
  const result = [];
  
  // 首先构建哈希映射
  for (const item of items) {
    itemMap[item.id] = { ...item, children: [] };
  }
  
  // 构建树结构
  for (const item of items) {
    const node = itemMap[item.id];
    if (item.parentId === 0) {
      result.push(node);
    } else {
      if (itemMap[item.parentId]) {
        itemMap[item.parentId].children.push(node);
      }
    }
  }
  
  return result;
}

const optimizedTree = buildTreeOptimized(flatData);
console.log(JSON.stringify(optimizedTree, null, 2));

优点

  • 时间复杂度O(n),性能优异
  • 适合大数据量处理

缺点

  • 需要额外的内存空间存储映射

方法3:使用Map数据结构(ES6+)

function buildTreeWithMap(items) {
  const map = new Map();
  const result = [];
  
  // 初始化所有节点并存入Map
  items.forEach(item => {
    map.set(item.id, { ...item, children: [] });
  });
  
  // 构建树结构
  items.forEach(item => {
    const node = map.get(item.id);
    if (item.parentId === 0) {
      result.push(node);
    } else {
      const parent = map.get(item.parentId);
      if (parent) {
        parent.children.push(node);
      }
    }
  });
  
  return result;
}

const mapTree = buildTreeWithMap(flatData);
console.log(JSON.stringify(mapTree, null, 2));

优点

  • 使用Map更现代,性能更好
  • 支持任意类型作为键

方法4:使用reduce实现(函数式编程)

function buildTreeWithReduce(items) {
  const itemMap = items.reduce((map, item) => {
    map[item.id] = { ...item, children: [] };
    return map;
  }, {});
  
  return items.reduce((result, item) => {
    if (item.parentId === 0) {
      result.push(itemMap[item.id]);
    } else if (itemMap[item.parentId]) {
      itemMap[item.parentId].children.push(itemMap[item.id]);
    }
    return result;
  }, []);
}

const reducedTree = buildTreeWithReduce(flatData);
console.log(JSON.stringify(reducedTree, null, 2));

优点

  • 函数式风格,代码简洁
  • 两次遍历完成构建

三、进阶功能实现

1. 添加排序功能

function buildTreeWithSort(items, sortBy = 'id') {
  const itemMap = {};
  const result = [];
  
  // 构建映射
  items.forEach(item => {
    itemMap[item.id] = { ...item, children: [] };
  });
  
  // 构建树结构
  items.forEach(item => {
    const node = itemMap[item.id];
    if (item.parentId === 0) {
      result.push(node);
    } else if (itemMap[item.parentId]) {
      itemMap[item.parentId].children.push(node);
    }
  });
  
  // 递归排序
  function sortTree(nodes) {
    nodes.sort((a, b) => a[sortBy] - b[sortBy]);
    nodes.forEach(node => {
      if (node.children.length) {
        sortTree(node.children);
      }
    });
  }
  
  sortTree(result);
  return result;
}

const sortedTree = buildTreeWithSort(flatData, 'id');
console.log(JSON.stringify(sortedTree, null, 2));

2. 处理循环引用

function buildTreeWithCycleDetection(items) {
  const itemMap = {};
  const result = [];
  const visited = new Set();
  
  // 构建映射
  items.forEach(item => {
    itemMap[item.id] = { ...item, children: [] };
  });
  
  // 检测循环引用
  function hasCycle(node, parentIds = new Set()) {
    if (parentIds.has(node.id)) return true;
    parentIds.add(node.id);
    
    for (const child of node.children) {
      if (hasCycle(child, new Set(parentIds))) {
        return true;
      }
    }
    
    return false;
  }
  
  // 构建树结构
  items.forEach(item => {
    if (!visited.has(item.id)) {
      const node = itemMap[item.id];
      if (item.parentId === 0) {
        result.push(node);
      } else if (itemMap[item.parentId]) {
        itemMap[item.parentId].children.push(node);
      }
      visited.add(item.id);
      
      // 检查循环引用
      if (hasCycle(node)) {
        throw new Error(`检测到循环引用,节点ID: ${node.id}`);
      }
    }
  });
  
  return result;
}

3. 支持自定义子节点字段名

function buildTreeCustomChildren(items, childrenField = 'children') {
  const itemMap = {};
  const result = [];
  
  // 构建映射
  items.forEach(item => {
    itemMap[item.id] = { ...item, [childrenField]: [] };
  });
  
  // 构建树结构
  items.forEach(item => {
    const node = itemMap[item.id];
    if (item.parentId === 0) {
      result.push(node);
    } else if (itemMap[item.parentId]) {
      itemMap[item.parentId][childrenField].push(node);
    }
  });
  
  return result;
}

四、性能优化技巧

  • 使用Map代替普通对象:当ID为非数字时性能更好
  • 批量处理数据:对于大数据量可分批次处理
  • 使用Web Worker:对于极大数据集可在后台线程处理
  • 惰性加载:只构建和渲染可见部分的树结构

五、实际应用示例

1. 渲染树形菜单(React示例)

function TreeMenu({ data }) {
  return (
    <ul>
      {data.map(node => (
        <li key={node.id}>
          {node.name}
          {node.children && node.children.length > 0 && (
            <TreeMenu data={node.children} />
          )}
        </li>
      ))}
    </ul>
  );
}

// 使用
const treeData = buildTreeOptimized(flatData);
ReactDOM.render(<TreeMenu data={treeData} />, document.getElementById('root'));

2. 树形表格(Vue示例)

<template>
  <table>
    <tbody>
      <tree-row 
        v-for="node in treeData" 
        :key="node.id" 
        :node="node"
        :level="0"
      />
    </tbody>
  </table>
</template>

<script>
import { buildTreeWithMap } from './treeUtils';

export default {
  data() {
    return {
      flatData: [...], // 原始扁平数据
      treeData: []
    };
  },
  created() {
    this.treeData = buildTreeWithMap(this.flatData);
  },
  components: {
    TreeRow: {
      props: ['node', 'level'],
      template: `
        <tr :style="{ paddingLeft: level * 20 + 'px' }">
          <td>{{ node.name }}</td>
          <td>{{ node.otherField }}</td>
        </tr>
        <tree-row 
          v-for="child in node.children" 
          :key="child.id" 
          :node="child"
          :level="level + 1"
          v-if="node.children"
        />
      `
    }
  }
};
</script>

六、总结与最佳实践

  1. 选择合适算法

    • 小数据量:递归算法简单直观
    • 大数据量:对象引用或Map实现性能更好
  2. 处理边界情况

    • 无效的parentId引用
    • 循环引用检测
    • 重复数据处理
  3. 扩展性考虑

    • 支持自定义子节点字段名
    • 添加排序功能
    • 支持异步数据加载
  4. 性能监控

    • 对于超大数据集考虑分页或虚拟滚动
    • 使用性能分析工具监控构建时间
  5. 测试建议

// 单元测试示例
describe('树形结构构建', () => {
  it('应正确构建树形结构', () => {
    const flatData = [...];
    const treeData = buildTreeOptimized(flatData);
    expect(treeData.length).toBe(2);
    expect(treeData[0].children.length).toBe(2);
    expect(treeData[0].children[1].children.length).toBe(2);
  });
  
  it('应处理无效parentId', () => {
    const invalidData = [...];
    const treeData = buildTreeOptimized(invalidData);
    expect(treeData.length).toBe(1);
  });
});

通过本文介绍的各种方法和技巧,您应该能够根据实际需求选择最适合的方式将扁平数据转换为树形结构。在实际项目中,推荐使用对象引用或Map实现的高效算法,它们在大数据量下表现优异,同时代码也相对清晰可维护。

以上就是使用JavaScript将扁平数据转换为树形结构的多种实现方法的详细内容,更多关于JavaScript扁平数据转树形结构的资料请关注脚本之家其它相关文章!

相关文章

  • JS调用打印方法设置页眉页脚的实例

    JS调用打印方法设置页眉页脚的实例

    一个网页打印相关功能的扩展演示特效,在实现了打印功能外,还实现了打印预览、打印前的页眉页脚设置,直接打印等功能,以前对JS打印前设置页脚见的不多,所以这一个也算是挺有价值的,希望闲暇时参阅
    2013-05-05
  • JavaScript中为元素加上name属性的方法

    JavaScript中为元素加上name属性的方法

    干前端这行当已经超过一个月了, 每天都会遇到新奇古怪, 甚至离奇的问题. 虽然绝大部分都是一些小问题, 但我觉得还是有必要记录下来
    2011-05-05
  • ESC之ESC.wsf可以实现javascript的代码压缩附使用方法

    ESC之ESC.wsf可以实现javascript的代码压缩附使用方法

    可以对javascript的大小进行压缩。使javascript的加载速度变快。
    2007-05-05
  • 前端给后端传数据的五种方式总结

    前端给后端传数据的五种方式总结

    前端与后端数据交互是软件开发中不可或缺的一环,下面这篇文章主要给大家介绍了关于前端给后端传数据的五种方式,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-07-07
  • javascript浏览器用户代理检测脚本实现方法

    javascript浏览器用户代理检测脚本实现方法

    下面小编就为大家带来一篇javascript浏览器用户代理检测脚本实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • js中script的上下放置区别,Dom的增删改创建操作实例分析

    js中script的上下放置区别,Dom的增删改创建操作实例分析

    这篇文章主要介绍了js中script的上下放置区别,Dom的增删改创建操作,结合实例形式分析了JavaScript基本dom事件、script在head和body中放置的区别、以及Dom的增删改创建等相关操作技巧,需要的朋友可以参考下
    2019-12-12
  • JavaScript实现深拷贝的不同方法汇总

    JavaScript实现深拷贝的不同方法汇总

    在JavaScript中,我们经常需要将一个对象的所有数据复制到另一个对象中,这种操作可以分为 浅拷贝 和 深拷贝,而深拷贝则是指完全复制一个对象及其所有嵌套的对象,而不仅仅是对象的引用,本文,我们将深入探讨深拷贝的概念,以及如何使用不同的方法实现深拷贝
    2025-06-06
  • hash特点、hashchange事件介绍及其常见应用场景

    hash特点、hashchange事件介绍及其常见应用场景

    浅析hash特点、hashchange事件介绍及其常见应用场景(不同hash对应不同事件处理、移动端大图展示状态后退页面问题、原生轻应用头部后退问题、移动端自带返回按钮二次确认问题),hashchange和popstate事件触发条件
    2023-11-11
  • 一文总结JS中逻辑运算符的特点

    一文总结JS中逻辑运算符的特点

    在JavaScript的众多运算符里,提供了三个逻辑运算符&&、||和!,下面这篇文章主要给大家介绍了关于JS中逻辑运算符的特点,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-03-03
  • layui使用button按钮 点击出现弹层 弹层中加载表单的实例

    layui使用button按钮 点击出现弹层 弹层中加载表单的实例

    今天小编就为大家分享一篇layui使用button按钮 点击出现弹层 弹层中加载表单的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-09-09

最新评论