JS 树与数组相互转换的几种方法
前言
在前端业务中,后端返回的扁平化数组(Array)往往需要转换为树形结构(Tree)来适配 UI 组件(如 Element UI 的 Tree 或 Cascader)。掌握多种转换思路及性能差异,是进阶高级前端的必备技能。
一、 核心概念:结构对比
数组结构:每一项通过 parentId 指向父级。
const nodes = [
{ id: 3, name: '节点C', parentId: 1 },
{ id: 6, name: '节点F', parentId: 3 },
{ id: 0, name: 'root', parentId: null },
{ id: 1, name: '节点A', parentId: 0 },
{ id: 8, name: '节点H', parentId: 4 },
{ id: 4, name: '节点D', parentId: 1 },
{ id: 2, name: '节点B', parentId: 0 },
{ id: 5, name: '节点E', parentId: 2 },
{ id: 7, name: '节点G', parentId: 2 },
{ id: 9, name: '节点I', parentId: 5 },
];
树形结构:父级通过 children 数组包裹子级。
let tree = [
{
id: 1,
name: 'text1',
parentId: 1,
children: [
{
id: 2,
name: 'text2',
parentId: 1,
children: [
{
id: 4,
name: 'text4',
parentId: 2,
},
],
},
{
id: 3,
name: 'text3',
parentId: 1,
},
],
},
];
二、 数组转树
1. 递归思路
原理:
- 首先需要传递给函数两个参数:数组、当前的父节点id
- 设置一个结果数组res,遍历数组,先找到子元素的父节点id与父节点id一致的子项
- 将这个子项的id作为父节点id传入函数,继续遍历
- 将遍历的结果作为children返回,并给当前项添加children
- 将这个当前项,插入到res里面,并返回
注意:如果不想影响原数组,需要先深拷贝一下数组。const cloneArr = JSON.parse(JSON.stringify (arr))
const nodes = [
{ id: 3, name: '节点C', parentId: 1 },
{ id: 6, name: '节点F', parentId: 3 },
{ id: 0, name: 'root', parentId: null },
{ id: 1, name: '节点A', parentId: 0 },
{ id: 8, name: '节点H', parentId: 4 },
{ id: 4, name: '节点D', parentId: 1 },
{ id: 2, name: '节点B', parentId: 0 },
{ id: 5, name: '节点E', parentId: 2 },
{ id: 7, name: '节点G', parentId: 2 },
{ id: 9, name: '节点I', parentId: 5 },
];
//递归写法
const arrToTree1 = (arr, id) => {
const res = [];
arr.forEach((item) => {
if (item.parentId === id) {
const children = arrToTree1(arr, item.id);
//如果希望每个元素都有children属性,可以直接赋值
if (children.length !== 0) {
item.children = children;
}
res.push(item);
}
});
return res;
};
console.log(arrToTree1(nodes, null));
2. 非递归思路
原理:利用 filter 进行二次筛选。虽然写法简洁,但在大数据量下性能较差(O(n2)O(n^2)O(n2))。
- 函数只需要接受一个参数,也就是需要转换的数组arr
- 第一层过滤数组,直接返回一个parentId为根id的元素
- 但是在返回之间,需要再根据当前id过滤里面的每一项(过滤规则为如果子项的paentId为当前的id,则在当前项的children插入这个子项)
const arrToTree2 = (arr) => {
return arr.filter((father) => {
const childrenArr = arr.filter((children) => {
return children.parentId === father.id;
});
//如果希望每个元素都有children属性,可以直接赋值
if (childrenArr.length !== 0) {
father.children = childrenArr;
}
return father.parentId === null;
});
};
console.log(arrToTree2(nodes));
3. Map 对象方案(O(n)O(n)O(n)时间复杂度)
原理:利用对象的引用性质。先将数组转为 Map,再遍历一次即可完成。这是在大数据量下的首选方案。
const arrToTree3 = (arr) => {
const map = {};
const res = [];
// 1. 建立映射表
arr.forEach((item) => {
map[item.id] = { ...item, children: [] };
});
// 2. 组装树结构
arr.forEach((item) => {
const node = map[item.id];
if (item.parentId === null) {
res.push(node);
} else {
if (map[item.parentId]) {
map[item.parentId].children.push(node);
}
}
});
return res;
};
console.log(arrToTree3(nodes));
三、 树转数组
1. 递归遍历思路
原理:定义一个结果数组,递归遍历树的每一层,将节点信息(排除 children)推入数组。
- 首先定义一个结果数组res,遍历传入的树
- 直接将当前项的id、name、parentId包装在一个新对象里插入
- 判断是否有children属性,如果有则遍历children属性每一项,继续执行2、3步骤
let tree = [
{
id: 1,
name: 'text1',
parentId: 1,
children: [
{
id: 2,
name: 'text2',
parentId: 1,
children: [
{
id: 4,
name: 'text4',
parentId: 2,
},
],
},
{
id: 3,
name: 'text3',
parentId: 1,
},
],
},
];
const treeToArr = (tree) => {
const res = [];
tree.forEach((item) => {
const loop = (data) => {
res.push({
id: data.id,
name: data.name,
parseId: data.parentId,
});
if (data.children) {
data.children.forEach((itemChild) => {
loop(itemChild);
});
}
};
loop(item);
});
return res;
};
console.log(treeToArr(tree));
四、 注意事项:深拷贝的必要性
在处理这些转换时,由于 JS 的对象是引用类型,直接修改 item.children 会改变原始数组的内容。
- 快捷方案:const cloneArr = JSON.parse(JSON.stringify(arr))。
- 避坑点:如果数组项中包含 Date 对象、RegExp 或 Function,JSON.parse 会导致数据失真,此时应使用其他深拷贝方案。
到此这篇关于JS 树与数组相互转换的几种方法的文章就介绍到这了,更多相关JS 树与数组相互转换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解用webpack的CommonsChunkPlugin提取公共代码的3种方式
本篇文章主要介绍了详解用webpack的CommonsChunkPlugin提取公共代码的3种方式,具有一定的参考价值,有兴趣的可以了解一下2017-11-11
Typescript tipe freshness 更严格对象字面量检查
这篇文章主要为大家介绍了Typescript tipe freshness 更严格对象字面量检查,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-10-10


最新评论