在实际开发中,我们经常会遇到将扁平数据(如数据库查询结果)转换为树形结构的需求,例如用于菜单栏、权限系统、组织架构等场景。本文将详细介绍如何使用 JavaScript 实现一个通用的二维数组转树形数组的方法,并提供完整的函数实现与示例。
问题描述
假设我们有一个扁平化的二维数组,其中每个元素都有一个唯一标识 id
和一个指向父节点的字段(如 parentId
),如下所示:
const list = [
{ id: 1, name: '一级菜单', parentId: null },
{ id: 2, name: '二级菜单A', parentId: 1 },
{ id: 3, name: '二级菜单B', parentId: 1 },
{ id: 4, name: '三级菜单A-1', parentId: 2 },
{ id: 5, name: '三级菜单A-2', parentId: 2 }
]
我们的目标是将其转换为具有嵌套 children
字段的树形结构:
[
{
id: 1,
name: '一级菜单',
parentId: null,
children: [
{
id: 2,
name: '二级菜单A',
parentId: 1,
children: [
{ id: 4, name: '三级菜单A-1', parentId: 2, children: [] }
]
},
...
]
}
]
解决方案:arrToTree
函数详解
下面是实现该功能的核心函数:
/**
* 将二维数组转为树形数组
* @param arr 数组
* @param idFieldName 节点唯一标识字段名,默认 'id'
* @param parentFieldName 父节点标识字段名,默认 'parent'
*/
export function arrToTree(arr = [], idFieldName = 'id', parentFieldName = 'parent') {
const map = {}
const tree = []
// 第一步:将所有节点放入 map 中,方便后续查找
arr.forEach(item => {
map[item[idFieldName]] = {
...item,
children: []
}
})
// 第二步:遍历数据,将每个节点挂到对应的 parentFieldName 下
arr.forEach(item => {
const node = map[item[idFieldName]]
if (item[parentFieldName] === null) {
// 根节点
tree.push(node)
} else {
// 找到父节点
const parent = map[item[parentFieldName]]
if (parent) {
parent.children.push(node)
}
}
})
return tree
}
函数说明
- 构建映射表
map
首次遍历数组时,我们将每个元素存入一个以id
为键的对象中,便于后续快速查找父节点。 - 二次遍历构建树结构
第二次遍历时,根据当前节点的parentId
查找其父节点,并将当前节点加入父节点的children
数组中。如果是根节点(即parentId
为null
),则直接添加到结果数组tree
中。
示例用法
const data = [
{ id: 1, name: '一级菜单', parentId: null },
{ id: 2, name: '二级菜单A', parentId: 1 },
{ id: 3, name: '二级菜单B', parentId: 1 },
{ id: 4, name: '三级菜单A-1', parentId: 2 },
]
const tree = arrToTree(data, 'id', 'parentId')
console.log(JSON.stringify(tree, null, 2))
输出结果如下:
[
{
"id": 1,
"name": "一级菜单",
"parentId": null,
"children": [
{
"id": 2,
"name": "二级菜单A",
"parentId": 1,
"children": [
{
"id": 4,
"name": "三级菜单A-1",
"parentId": 2,
"children": []
}
]
},
{
"id": 3,
"name": "二级菜单B",
"parentId": 1,
"children": []
}
]
}
]
+1