在前端业务开发中,后端接口常返回扁平的数组数据,比如菜单项、部门列表等,而前端渲染时需要层级化的树形JSON结构,递归是实现这种转换的高效方式。递归的核心是通过不断调用自身函数,逐层查找父子节点的关联关系,最终拼接出完整的树形结构。

前置数据结构准备
首先我们需要明确扁平数据和目标树形数据的结构,假设后端返回的扁平数据包含以下字段:
- id:当前节点的唯一标识
- parentId:当前节点的父节点标识,顶级节点的parentId为null或者0
- name:节点展示名称
- 其他自定义扩展字段
扁平数据示例如下:
// 扁平数组数据
const flatList = [
{ id: 1, parentId: null, name: '总部' },
{ id: 2, parentId: 1, name: '技术部' },
{ id: 3, parentId: 1, name: '产品部' },
{ id: 4, parentId: 2, name: '前端组' },
{ id: 5, parentId: 2, name: '后端组' },
{ id: 6, parentId: 3, name: '需求组' }
];
我们的目标是将其转换为如下树形结构:
[
{
"id": 1,
"parentId": null,
"name": "总部",
"children": [
{
"id": 2,
"parentId": 1,
"name": "技术部",
"children": [
{ "id": 4, "parentId": 2, "name": "前端组", "children": [] },
{ "id": 5, "parentId": 2, "name": "后端组", "children": [] }
]
},
{
"id": 3,
"parentId": 1,
"name": "产品部",
"children": [
{ "id": 6, "parentId": 3, "name": "需求组", "children": [] }
]
}
]
}
]
递归转换核心思路
递归构建树形结构的核心逻辑可以分为三步:
- 首先筛选出所有顶级节点,也就是parentId为null或者指定顶级标识的节点
- 为每个节点添加children属性,初始值为空数组
- 对每个节点递归查找其所有子节点,将子节点放入当前节点的children数组中
递归函数实现
我们首先实现一个递归查找子节点的函数,函数的入参是当前父节点和完整的扁平数据列表:
/**
* 递归查找当前节点的所有子节点
* @param {Object} parentNode 当前父节点
* @param {Array} flatData 完整的扁平数据列表
* @returns {Array} 当前节点的子节点数组
*/
function findChildren(parentNode, flatData) {
// 筛选出所有parentId等于当前节点id的子节点
const children = flatData.filter(item => item.parentId === parentNode.id);
// 遍历每个子节点,递归查找其子节点
children.forEach(child => {
// 为子节点添加children属性
child.children = findChildren(child, flatData);
});
return children;
}
接下来我们实现主转换函数,先筛选顶级节点,再为每个顶级节点调用递归函数:
/**
* 将扁平数据转换为树形结构
* @param {Array} flatData 扁平数据数组
* @param {*} topParentId 顶级节点的parentId值,默认null
* @returns {Array} 树形结构数组
*/
function flatToTree(flatData, topParentId = null) {
// 筛选所有顶级节点
const topNodes = flatData.filter(item => item.parentId === topParentId);
// 为每个顶级节点递归查找子节点
topNodes.forEach(node => {
node.children = findChildren(node, flatData);
});
return topNodes;
}
完整调用示例
我们使用前面的扁平数据调用转换函数,查看输出结果:
// 调用转换函数 const treeData = flatToTree(flatList); // 打印转换后的树形数据 console.log(JSON.stringify(treeData, null, 2));
执行上述代码后,控制台会输出我们预期的目标树形JSON结构,说明转换逻辑正确。
边界情况处理
在实际开发中,可能会遇到一些特殊情况需要处理:
- 如果扁平数据中存在无效的parentId,也就是找不到对应的父节点,可以选择忽略这些节点,或者将其归为顶级节点,根据实际业务需求调整筛选逻辑
- 如果数据量较大,递归深度过深可能会导致栈溢出,这时候可以考虑使用循环+映射表的方式替代递归,提升性能
- 如果原始数据中没有children字段,需要在转换时统一添加,避免后续渲染时出现undefined错误
优化方案:使用映射表提升性能
前面的递归实现需要多次遍历扁平数组,数据量较大时性能较差,我们可以通过构建id到节点的映射表,减少遍历次数:
/**
* 使用映射表优化的树形转换函数
* @param {Array} flatData 扁平数据数组
* @param {*} topParentId 顶级节点的parentId值,默认null
* @returns {Array} 树形结构数组
*/
function flatToTreeOptimized(flatData, topParentId = null) {
// 构建id到节点的映射表
const nodeMap = {};
flatData.forEach(item => {
nodeMap[item.id] = { ...item, children: [] };
});
const tree = [];
flatData.forEach(item => {
const node = nodeMap[item.id];
if (item.parentId === topParentId) {
// 顶级节点直接放入结果数组
tree.push(node);
} else {
// 非顶级节点找到父节点,放入父节点的children中
const parent = nodeMap[item.parentId];
if (parent) {
parent.children.push(node);
}
}
});
return tree;
}
这种方式只需要遍历两次扁平数据,时间复杂度为O(n),比递归遍历的性能更好,适合处理大数据量的场景。
总结
递归是处理树形结构转换的直观方式,核心是通过父子节点标识的关联,逐层拼接节点关系。对于小数据量场景,递归实现简单易懂;对于大数据量场景,可以使用映射表优化的方式提升性能。开发者可以根据实际业务需求选择合适的实现方案,同时注意处理无效数据和递归深度等边界情况,保证代码的健壮性。
JavaScript递归JSON树形结构扁平数据转换修改时间:2026-06-17 11:39:41