前端实用技巧:一行代码实现行数据转树形结构

2026-01-31 47 浏览 0 评论

在前端开发中,处理层级数据是非常常见的需求,比如构建菜单导航、组织部门架构、展示分类目录等场景,都需要将扁平的行数据转换为嵌套的树形结构。本文将详细拆解一个简洁高效的行转树工具函数,帮助你理解其核心逻辑,并能灵活应用到实际开发中。

一、函数功能与使用场景

先来看我们要实现的核心函数 list2tree ,它的核心作用是: 将包含父级关联关系的扁平数组(行数据),转换为带有层级嵌套的树形结构数组

适用场景包括但不限于:

  • 前端菜单渲染(一级菜单、二级菜单嵌套)
  • 部门/组织架构展示
  • 商品分类层级展示
  • 评论区的嵌套回复结构

二、完整函数代码

先贴出完整的函数代码,方便你整体感知:

/**
 * 将扁平的行数据转换为树形结构
 * @param {Array} list - 原始扁平数组
 * @param {Number} parent_id - 根节点的父 ID,默认 0
 * @param {String} target_field - 节点唯一标识字段,默认'id'
 * @param {String} parent_field - 父节点关联字段,默认'parent_id'
 * @returns {Array} 嵌套的树形结构数组
 */
list2tree(list, parent_id = 0, target_field = 'id', parent_field = 'parent_id') {
    // 1. 创建节点映射对象,用于快速查找节点
    const menuObj = {};
    list.forEach(item => {
      // 处理父 ID 为空的情况,默认赋值 0
      item[parent_field] = item[parent_field] || 0;
      // 以节点唯一标识为键,存储节点本身,方便后续快速查找
      menuObj[item[target_field]] = item;
    });

    // 2. 筛选根节点并构建子节点嵌套关系
    return list.filter(item => {
      // 判断当前节点是否为根节点(父 ID 不等于根节点父 ID)
      if (item[parent_field] !== parent_id) {
        // 找到当前节点的父节点,为其添加 children 属性
        if ('children' in menuObj[item[parent_field]]) {
          // 父节点已有 children,直接 push 当前节点
          menuObj[item[parent_field]].children.push(item);
        } else {
          // 父节点无 children,初始化数组并放入当前节点
          menuObj[item[parent_field]].children = [item];
        }
        // 非根节点过滤掉,最终只返回根节点数组
        return false;
      }
      // 根节点保留,返回 true
      return true;
    });
  },

三、代码逐段拆解说明

1. 函数参数定义

list2tree(list, parent_id = 0, target_field = 'id', parent_field = 'parent_id') {
  • list :必传参数,需要转换的原始扁平数组,每一项是包含节点信息的对象;
  • parent_id :可选参数,根节点的父 ID 值,默认 0(可根据业务调整,比如部分场景根节点父 ID 为 null 或'');
  • target_field :可选参数,节点的唯一标识字段,默认'id'(比如业务中可能用'uid'、'menuId'等);
  • parent_field :可选参数,节点关联父节点的字段,默认'parent_id'(比如业务中可能用'pid'、'parentId'等)。

2. 构建节点映射对象

const menuObj = {};
list.forEach(item => {
  // 兼容父 ID 为空的情况,统一赋值为 0,避免匹配不到根节点
  item[parent_field] = item[parent_field] || 0;
  // 以节点唯一 ID 为键,存储节点本身,O(1) 时间复杂度查找节点
  menuObj[item[target_field]] = item;
});

这一步是核心优化点:

  • 遍历原始数组,将每个节点以「唯一 ID-节点对象」的形式存入 menuObj
  • 后续查找父节点时无需再次遍历数组,直接通过键值对获取,将时间复杂度从 O(n²) 优化为 O(n);
  • 同时处理父 ID 为空的边界情况,统一赋值为 0,避免因父 ID 为 null/undefined 导致的匹配失败。

3. 筛选根节点+构建嵌套关系

return list.filter(item => {
  if (item[parent_field] !== parent_id) {
    // 非根节点:找到父节点并添加到 children 中
    if ('children' in menuObj[item[parent_field]]) {
      menuObj[item[parent_field]].children.push(item);
    } else {
      menuObj[item[parent_field]].children = [item];
    }
    return false; // 过滤掉非根节点
  }
  return true; // 保留根节点
});

这一步通过 filter 方法完成两个核心操作:

  • 筛选根节点 :判断节点的父 ID 是否等于预设的根节点父 ID(默认 0),等于则为根节点,保留;
  • 构建子节点嵌套 :非根节点找到对应的父节点,为父节点添加 children 属性,将当前节点放入父节点的 children 数组中;
  • filter 方法的特性:返回 true 的项会保留在结果数组中,返回 false 的项会被过滤,最终结果只包含根节点,且每个根节点下已嵌套好所有子节点。

四、使用示例

1. 准备原始数据

const rawList = [
  { id: 1, name: '首页', parent_id: 0 },
  { id: 2, name: '用户管理', parent_id: 0 },
  { id: 3, name: '用户列表', parent_id: 2 },
  { id: 4, name: '新增用户', parent_id: 3 },
  { id: 5, name: '角色管理', parent_id: 2 }
];

2. 调用函数转换

const treeData = list2tree(rawList);
console.log(treeData);

3. 输出结果

[
  { id: 1, name: '首页', parent_id: 0 },
  { 
    id: 2, 
    name: '用户管理', 
    parent_id: 0, 
    children: [
      { 
        id: 3, 
        name: '用户列表', 
        parent_id: 2, 
        children: [{ id: 4, name: '新增用户', parent_id: 3 }] 
      },
      { id: 5, name: '角色管理', parent_id: 2 }
    ]
  }
]

五、扩展与优化建议

  1. 支持多级嵌套 :当前函数天然支持无限层级嵌套,只要原始数据的父 ID 关联正确,即可生成任意深度的树形结构;
  2. 空数据处理 :可添加入参校验,避免传入空数组时报错:
   if (!Array.isArray(list) || list.length === 0) return [];
  1. 防循环引用 :如果原始数据中存在父 ID 指向自身的异常数据,可添加判断:
   if (item[target_field] === item[parent_field]) return false;
  1. 自定义 children 字段 :可新增参数 children_field ,让用户自定义子节点字段名(比如'child'、'subMenu')。

总结

  1. 核心逻辑:先通过映射对象实现节点快速查找,再通过 filter 筛选根节点并构建子节点嵌套,时间复杂度优化至 O(n);
  2. 灵活性:通过参数配置支持自定义节点唯一标识、父关联字段、根节点父 ID,适配不同业务场景;
  3. 边界处理:兼容父 ID 为空的情况,避免转换失败,保证函数的健壮性。

这个工具函数代码简洁、性能高效,可直接集成到你的前端项目中,解决大部分行数据转树形结构的需求。


发布评论

发布评论前请先 登录

评论列表 0

暂无评论