
本文详解如何使用 JavaScript 的 reduce 与递归结合,将含 id/parentId 关系的扁平对象数组(如组织架构、设备层级等)转换为标准嵌套树结构,支持任意深度,并避免常见误区(如仅按类型分组而非真实父子关系)。
本文详解如何使用 javascript 的 reduce 与递归结合,将含 `id`/`parentid` 关系的扁平对象数组(如组织架构、设备层级等)转换为标准嵌套树结构,支持任意深度,并避免常见误区(如仅按类型分组而非真实父子关系)。
在实际开发中(如设备管理、菜单配置、组织架构渲染),我们常遇到一类数据:它以扁平数组形式存储具有父子关系的节点,每个对象包含唯一标识 id 和指向父节点的 parentId。目标是将其转化为符合 DOM 渲染或组件库(如 Ant Design Tree、Element Plus ElTree)要求的标准树形结构——即每个节点通过 children 数组持有其直接子节点,且根节点 parentId 通常为 "0" 或 null。
你之前使用的 reduce 方法本质是按 type 字段做分类聚合,生成的是横向分组结构(如 { Building: [...], Room: [...] }),而非按 parentId → id 构建的纵向层级结构。正确解法需抓住两个核心逻辑:
- ✅ 定位根节点:筛选出所有 parentId === "0"(或指定根 ID)的项作为顶层入口;
- ✅ 递归挂载子树:对每个节点,再次遍历原数组,找出所有 parentId 等于当前节点 id 的子项,并递归构建其 children。
以下是推荐的简洁、健壮、可读性强的实现方案:
/**
* 将扁平数组转换为树形结构
* @param {Array} data - 源数据数组,每个对象需含 id 和 parentId 字段
* @param {string|number|null} [parentId="0"] - 根节点的 parentId 值(默认 "0")
* @returns {Array} 树形结构数组(森林,可能含多个根)
*/
function toTree(data, parentId = "0") {
return data.reduce((acc, node) => {
// 当前节点是目标 parentId 的子节点时,纳入结果
if (node.parentId == parentId) {
// 浅拷贝节点,避免污染原始数据
const newNode = { ...node };
// 递归构建 children 数组
newNode.children = toTree(data, node.id);
acc.push(newNode);
}
return acc;
}, []);
}
// 使用示例
const arrData = [
{ id: "3", name: "Ctech A", parentId: "0", type: "Building" },
{ id: "4", name: "3rd floor", parentId: "3", type: "Floor" },
{ id: "5", name: "room_01", parentId: "4", type: "Room" },
{ id: "6", name: "room_video", parentId: "4", type: "Room" },
{ id: "9", name: "Rack_1", parentId: "5", type: "Rack" },
{ id: "12", name: "Shelf_01", parentId: "9", type: "Shelf" },
{ id: "13", name: "Slot_1", parentId: "12", type: "Slot" }
];
const tree = toTree(arrData);
console.log(JSON.stringify(tree, null, 2));
✅ 关键优势与注意事项
- 时间复杂度可控:虽为递归 + 双重遍历(O(n²) 最坏情况),但对千级以内节点性能无压力;如需极致性能(万级+),建议先用 Map 预建 id → nodes[] 索引,将查找优化至 O(1);
- 健壮性设计:使用 == 而非 === 兼容字符串/数字型 ID(如 parentId: 0 与 "0");若需严格类型匹配,可改为 node.parentId === String(parentId);
- 空 children 处理:当某节点无子节点时,toTree(data, nodeId) 返回空数组 [],符合规范,无需额外过滤;
- 避免副作用:始终返回新对象({...node}),不修改原始数组,保障函数纯度;
- 支持多根树:若数据含多个 parentId === "0" 节点(如多个独立建筑),结果将为森林(数组含多个根节点),符合真实业务场景。
? 进阶建议
- 若需支持动态加载(懒加载子节点),可将 children 替换为 isLeaf: boolean + 异步 loadChildren() 方法;
- 结合 TypeScript 可定义泛型接口,提升类型安全:
interface TreeNode<t any> { id: string | number; parentId: string | number | null; children?: TreeNode<t>[]; [key: string]: T | string | number | null | TreeNode<t>[] | undefined; } function toTree<t extends treenode>(data: T[], parentId: string | number = "0"): T[] { ... }</t></t></t></t>
掌握这一模式,你便能灵活应对各类层级化数据建模需求——从机房设备拓扑到企业组织图谱,皆可一招制胜。










