扁平化
JavaScript 中树结构转换是常见需求,主要包括「数组转树」和「树转数组(扁平化)」两种场景。以下是详细示例,包含完整实现和应用场景说明。
1. 数组转树结构(核心场景)
将扁平数组(含 id 和 parentId)转换为嵌套树结构,适用于后端返回扁平数据,前端需要层级展示的场景。
(1)基础数据定义
假设有如下扁平数组(通常来自后端接口):
javascript
// 扁平数组(含 id 和 parentId,parentId 为 null 表示根节点)
const flatData = [
{ id: 1, name: "技术部", parentId: null },
{ id: 2, name: "产品部", parentId: null },
{ id: 11, name: "前端组", parentId: 1 },
{ id: 12, name: "后端组", parentId: 1 },
{ id: 111, name: "张三", parentId: 11 },
{ id: 112, name: "李四", parentId: 11 },
{ id: 121, name: "王五", parentId: 12 },
{ id: 21, name: "赵六", parentId: 2 },
];目标:转换为嵌套树结构(每个节点包含 children 数组)。
(2)实现方法(高效版)
使用「哈希表映射」+「一次遍历」实现,时间复杂度 O(n):
javascript
/**
* 扁平数组转树结构
* @param {Array} flatArray - 扁平数组(每个元素需包含 id 和 parentId)
* @param {string|number|null} rootParentId - 根节点的 parentId(默认 null)
* @returns {Array} 转换后的树结构
*/
function arrayToTree(flatArray, rootParentId = null) {
// 1. 创建哈希表(id 到节点的映射),方便快速查找父节点
const nodeMap = new Map();
// 2. 初始化所有节点(添加空 children)
flatArray.forEach((item) => {
nodeMap.set(item.id, { ...item, children: [] });
});
// 3. 构建树(关联父子节点)
const tree = [];
flatArray.forEach((item) => {
const currentNode = nodeMap.get(item.id);
const parentId = item.parentId;
if (parentId === rootParentId) {
// 根节点:直接加入树
tree.push(currentNode);
} else {
// 子节点:添加到父节点的 children 中
const parentNode = nodeMap.get(parentId);
if (parentNode) {
// 防止 parentId 无效的情况
parentNode.children.push(currentNode);
}
}
});
return tree;
}(3)使用示例
javascript
// 转换为树结构
const tree = arrayToTree(flatData);
console.log("数组转树结果:", tree);输出结果(嵌套树结构):
javascript
[
{
id: 1,
name: "技术部",
parentId: null,
children: [
{
id: 11,
name: "前端组",
parentId: 1,
children: [
{ id: 111, name: "张三", parentId: 11, children: [] },
{ id: 112, name: "李四", parentId: 11, children: [] },
],
},
{
id: 12,
name: "后端组",
parentId: 1,
children: [{ id: 121, name: "王五", parentId: 12, children: [] }],
},
],
},
{
id: 2,
name: "产品部",
parentId: null,
children: [{ id: 21, name: "赵六", parentId: 2, children: [] }],
},
];2. 树结构转扁平数组(反向转换)
将嵌套树结构转换为扁平数组(含 id 和 parentId),适用于前端编辑树后,需要提交扁平数据给后端的场景。
(1)实现方法(基于 DFS 遍历)
javascript
/**
* 树结构转扁平数组
* @param {Array} tree - 树结构数组
* @param {string|number|null} parentId - 父节点 id(用于递归传递)
* @returns {Array} 转换后的扁平数组
*/
function treeToArray(tree, parentId = null) {
const flatArray = [];
// 递归遍历树节点
function traverse(node, currentParentId) {
// 1. 提取当前节点信息(移除 children,添加 parentId)
const { children, ...nodeWithoutChildren } = node;
flatArray.push({ ...nodeWithoutChildren, parentId: currentParentId });
// 2. 递归处理子节点(父 id 为当前节点 id)
if (children && children.length > 0) {
children.forEach((child) => {
traverse(child, node.id);
});
}
}
// 遍历根节点(父 id 为 null)
tree.forEach((rootNode) => {
traverse(rootNode, parentId);
});
return flatArray;
}(2)使用示例
javascript
// 使用上面转换后的 tree 进行反向转换
const flatArrayAgain = treeToArray(tree);
console.log("树转数组结果:", flatArrayAgain);输出结果(与原始扁平数组一致):
javascript
[
{ id: 1, name: "技术部", parentId: null },
{ id: 11, name: "前端组", parentId: 1 },
{ id: 111, name: "张三", parentId: 11 },
{ id: 112, name: "李四", parentId: 11 },
{ id: 12, name: "后端组", parentId: 1 },
{ id: 121, name: "王五", parentId: 12 },
{ id: 2, name: "产品部", parentId: null },
{ id: 21, name: "赵六", parentId: 2 },
];3. 特殊场景处理
(1)处理不规范数据(如缺失 parentId 或循环引用)
javascript
function safeArrayToTree(flatArray, rootParentId = null) {
const nodeMap = new Map();
const invalidNodes = []; // 存储无效节点(如无 id 或 parentId 不存在)
// 1. 过滤无效节点(无 id 的节点)
flatArray.forEach((item) => {
if (item.id === undefined || item.id === null) {
invalidNodes.push(item); // 记录无效节点
return;
}
nodeMap.set(item.id, { ...item, children: [] });
});
// 2. 构建树(跳过 parentId 无效的节点)
const tree = [];
flatArray.forEach((item) => {
if (item.id === undefined || item.id === null) return; // 跳过无效节点
const currentNode = nodeMap.get(item.id);
const parentId = item.parentId;
if (parentId === rootParentId) {
tree.push(currentNode);
} else {
const parentNode = nodeMap.get(parentId);
if (parentNode) {
parentNode.children.push(currentNode);
} else {
invalidNodes.push(currentNode); // 父节点不存在,标记为无效
}
}
});
// 打印警告信息(可选)
if (invalidNodes.length > 0) {
console.warn("存在无效节点(未加入树中):", invalidNodes);
}
return tree;
}(2)带额外字段转换(如保留层级信息)
转换时添加 level 字段记录节点层级:
javascript
function treeToArrayWithLevel(tree) {
const flatArray = [];
function traverse(node, parentId, level) {
const { children, ...rest } = node;
flatArray.push({ ...rest, parentId, level }); // 新增 level 字段
if (children && children.length) {
children.forEach((child) => traverse(child, node.id, level + 1));
}
}
tree.forEach((root) => traverse(root, null, 1)); // 根节点层级为 1
return flatArray;
}
// 使用示例
const flatWithLevel = treeToArrayWithLevel(tree);
console.log("带层级的扁平数组:", flatWithLevel);输出结果(包含 level 字段):
javascript
[
{ id: 1, name: "技术部", parentId: null, level: 1 },
{ id: 11, name: "前端组", parentId: 1, level: 2 },
{ id: 111, name: "张三", parentId: 11, level: 3 },
// ... 其他节点
];4. 应用场景总结
| 转换方向 | 应用场景 | 核心逻辑 |
|---|---|---|
| 数组 → 树 | 后端返回扁平数据,前端需要层级展示(如菜单、组织架构) | 哈希表映射 + 父子关联 |
| 树 → 数组 | 前端编辑树结构后,提交数据给后端(通常需要扁平格式) | DFS 遍历 + 记录 parentId |
总结
- 数组转树的关键是用哈希表快速查找父节点,避免嵌套循环,提高效率。
- 树转数组的关键是递归遍历树节点,同时记录每个节点的父 ID。
- 实际开发中需处理边界情况(如无效节点、循环引用),保证转换稳定性。
以上方法可直接用于生产环境,根据业务需求调整字段(如用 key 代替 id,pid 代替 parentId)即可。