在 JavaScript 中,将普通数组数据转化为树形结构的数据是一个常见的任务,特别是在处理层级数据(例如分类、组织结构等)时。下面是一个详细的步骤说明,展示如何将一个扁平的数组转化为树形数据结构。
示例数据
假设我们有以下的扁平数组,每个元素代表一个节点,并且每个节点包含一个 id
和一个 parentId
,parentId
用于表示父节点的关系:
const data = [
{ id: 1, name: 'Root', parentId: null },
{ id: 2, name: 'Child 1', parentId: 1 },
{ id: 3, name: 'Child 2', parentId: 1 },
{ id: 4, name: 'Grandchild 1', parentId: 2 },
{ id: 5, name: 'Grandchild 2', parentId: 2 },
];
目标
将上述数据转化为以下树形结构:
[
{
id: 1,
name: 'Root',
children: [
{
id: 2,
name: 'Child 1',
children: [
{ id: 4, name: 'Grandchild 1', children: [] },
{ id: 5, name: 'Grandchild 2', children: [] }
]
},
{
id: 3,
name: 'Child 2',
children: []
}
]
}
]
实现步骤
-
创建一个空对象来存储每个节点的引用:
const nodeMap = {};
-
遍历数组,初始化节点并填充
nodeMap
:data.forEach(item => { nodeMap[item.id] = { ...item, children: [] }; });
这样每个节点都被映射到
nodeMap
中,并且每个节点都有一个children
属性用于存储子节点。 -
遍历数组,将每个节点添加到其父节点的
children
中:data.forEach(item => { if (item.parentId) { // 如果节点有父节点,将其添加到父节点的 children 中 const parent = nodeMap[item.parentId]; if (parent) { parent.children.push(nodeMap[item.id]); } } });
-
提取根节点:
根节点是
parentId
为null
的节点。我们可以从nodeMap
中找出这些节点。const tree = Object.values(nodeMap).filter(node => node.parentId === null);
完整代码示例
以下是将上述步骤整合在一起的完整代码:
const data = [
{ id: 1, name: 'Root', parentId: null },
{ id: 2, name: 'Child 1', parentId: 1 },
{ id: 3, name: 'Child 2', parentId: 1 },
{ id: 4, name: 'Grandchild 1', parentId: 2 },
{ id: 5, name: 'Grandchild 2', parentId: 2 },
];
function buildTree(data) {
const nodeMap = {};
// 初始化 nodeMap
data.forEach(item => {
nodeMap[item.id] = { ...item, children: [] };
});
// 构建树形结构
data.forEach(item => {
if (item.parentId) {
const parent = nodeMap[item.parentId];
if (parent) {
parent.children.push(nodeMap[item.id]);
}
}
});
// 提取根节点
return Object.values(nodeMap).filter(node => node.parentId === null);
}
const tree = buildTree(data);
console.log(JSON.stringify(tree, null, 2));
解释
-
初始化节点映射:
nodeMap
用于存储每个节点的引用,并初始化每个节点的children
为空数组。
-
建立父子关系:
- 遍历数据,找到每个节点的父节点,并将当前节点添加到父节点的
children
中。
- 遍历数据,找到每个节点的父节点,并将当前节点添加到父节点的
-
提取根节点:
- 通过过滤
nodeMap
中parentId
为null
的节点,得到树的根节点。
- 通过过滤
递归函数实现
function buildTree(flatData, parentId = null) {
return flatData
.filter(node => node.parentId === parentId)
.map(node => ({
...node,
children: buildTree(flatData, node.id)
}));
}
参数说明
flatData
: 扁平的数组数据。每个元素包含id
和parentId
,用于表示节点和它们的父节点关系。parentId
: 当前要处理的父节点的 ID。默认为null
,表示初始调用时的根节点。
函数实现细节
-
过滤节点:
flatData.filter(node => node.parentId === parentId)
filter
方法会返回flatData
中所有parentId
等于当前parentId
的节点。这意味着我们在寻找所有直接子节点。- 初次调用时,
parentId
是null
,所以我们得到的是所有根节点。
-
映射节点:
.map(node => ({ ...node, children: buildTree(flatData, node.id) }))
map
方法对过滤后的每个节点执行回调函数。- 回调函数的目的是将当前节点的
children
属性设置为一个递归调用buildTree
函数的结果。
-
递归调用:
buildTree(flatData, node.id)
- 对于每个节点,我们再次调用
buildTree
函数,将当前节点的id
作为新的parentId
。 - 这会找到当前节点的所有子节点,并将这些子节点作为当前节点的
children
属性。
- 对于每个节点,我们再次调用
递归过程
-
初始调用:
buildTree(flatData)
- 第一次调用时
parentId
是null
,这意味着我们查找所有根节点。
- 第一次调用时
-
查找子节点:
对于每一个根节点,
buildTree
会被调用,查找该节点的直接子节点。 -
逐级递归:
对每个子节点,
buildTree
会递归调用自身,继续查找该子节点的子节点,直到所有节点的children
属性都被填充。
示例
假设我们有以下扁平数据:
const data = [
{ id: 1, name: 'Root', parentId: null },
{ id: 2, name: 'Child 1', parentId: 1 },
{ id: 3, name: 'Child 2', parentId: 1 },
{ id: 4, name: 'Grandchild 1', parentId: 2 },
{ id: 5, name: 'Grandchild 2', parentId: 2 }
];
调用 buildTree(data)
时:
-
第一层:
parentId
是null
,找到 ID 为1
的根节点。
-
第二层:
- 对于根节点(ID 为
1
),调用buildTree(data, 1)
查找其子节点,找到 ID 为2
和3
的节点。
- 对于根节点(ID 为
-
第三层:
- 对于 ID 为
2
的节点,调用buildTree(data, 2)
查找其子节点,找到 ID 为4
和5
的节点。
- 对于 ID 为
-
第四层:
- ID 为
4
和5
的节点没有子节点,所以递归终止,返回空的children
数组。
- ID 为
最终得到的树形数据结构如下:
[
{
id: 1,
name: 'Root',
children: [
{
id: 2,
name: 'Child 1',
children: [
{ id: 4, name: 'Grandchild 1', children: [] },
{ id: 5, name: 'Grandchild 2', children: [] }
]
},
{
id: 3,
name: 'Child 2',
children: []
}
]
}
]
总结
这个 buildTree
函数使用了递归的方式来构建树形数据结构。通过过滤、映射和递归调用,它逐层构建每个节点的子节点,直到所有节点的 children
属性都被正确填充。这种方法简洁且高效,适合处理层级数据。