将扁平菜单转为树形结构需两次遍历:首次用对象缓存节点并初始化children,识别根节点;第二次将非根节点挂载到父节点children下,时间复杂度o(n)。

后端返回的菜单数据通常是扁平结构的数组,每个对象带 id、parentId(或 pid)、name 等字段。要渲染侧边栏、导航栏或树形选择器,需将其转为嵌套的树形结构。核心思路是:**用 Map 或对象缓存所有节点,再遍历一次,把子节点挂到对应父节点的 children 数组下**。
识别根节点并建立映射表
先遍历一次原始数组,区分根节点(如 parentId === null、parentId === 0 或 parentId === undefined)和普通节点。同时用 JavaScript 对象(或 Map)以 id 为键缓存每个节点,方便后续快速查找父节点:
- 推荐用普通对象
{},键为字符串化id(如map[item.id] = item),简单高效 - 注意检查
parentId的实际值——有些接口用"0"字符串,有些用数字0,有些用null,需统一判断逻辑 - 给每个节点初始化空
children: [],避免后续 push 时报错
单次遍历挂载子节点
再次遍历数组,对每个节点:
- 若它是根节点(如
!item.parentId || item.parentId === '0' || item.parentId === 0),推入结果数组 - 否则,查出其父节点(
map[item.parentId]),将当前节点push到父节点的children中
处理多级与排序需求
如果菜单要求按层级顺序展示(如一级菜单在前,其子菜单紧随其后),可在生成树后做一次 DFS 遍历展平并记录层级;若需前端控制展开/选中状态,建议在转换时就注入额外字段,例如:
-
level: 0表示根,每深入一层 +1 -
path: ['1', '5', '12']记录从根到当前节点的 id 路径,便于高亮或定位 -
isLeaf: !item.children?.length标记是否为叶子节点,简化模板判断
兼容性与边界情况提醒
真实项目中常见坑点:
- 数据存在环引用(A 的 parent 是 B,B 的 parent 是 A)——转换前可加深度限制或 visited 集合检测
- 父节点在数组中出现在子节点之后——没关系,因为用的是映射表查找,不是顺序依赖
- id 或 parentId 类型不一致(字符串 vs 数字)——建议统一转为字符串比较,或使用宽松相等
==(但需确认业务允许) - 后端返回空数组或 null —— 转换函数开头加守卫:
if (!Array.isArray(list) || list.length === 0) return []











