树形结构转成列表(处理菜单)
将嵌套的树形结构数据扁平化为一维数组,常用于菜单权限处理、数据展示等场景
问题
在前端开发中,我们经常会遇到树形结构的数据(如菜单、组织架构等),这些数据通常具有父子嵌套关系。但在某些场景下,我们需要将树形结构转换为扁平的列表结构,比如:
- 权限校验时需要获取所有菜单项
- 搜索功能需要遍历所有节点
- 数据导出需要扁平化处理
- 面包屑导航需要查找路径
本题要求实现一个函数,将树形结构的菜单数据转换为一维数组列表。
解答
方法一:递归实现(深度优先)
/**
* 树形结构转列表 - 递归实现
* @param {Array} tree - 树形结构数据
* @param {String} childrenKey - 子节点的键名,默认为 'children'
* @returns {Array} 扁平化后的列表
*/
function treeToList(tree, childrenKey = 'children') {
const result = [];
function traverse(nodes, parent = null) {
nodes.forEach(node => {
// 提取当前节点(不包含 children)
const { [childrenKey]: children, ...item } = node;
// 可选:添加父节点信息
if (parent) {
item.parentId = parent.id;
}
result.push(item);
// 递归处理子节点
if (children && children.length > 0) {
traverse(children, node);
}
});
}
traverse(tree);
return result;
}
方法二:迭代实现(广度优先)
/**
* 树形结构转列表 - 迭代实现
* @param {Array} tree - 树形结构数据
* @param {String} childrenKey - 子节点的键名,默认为 'children'
* @returns {Array} 扁平化后的列表
*/
function treeToListIterative(tree, childrenKey = 'children') {
const result = [];
const queue = [...tree]; // 使用队列存储待处理节点
while (queue.length > 0) {
const node = queue.shift();
const { [childrenKey]: children, ...item } = node;
result.push(item);
// 将子节点加入队列
if (children && children.length > 0) {
queue.push(...children);
}
}
return result;
}
方法三:保留层级信息
/**
* 树形结构转列表 - 保留层级和路径信息
* @param {Array} tree - 树形结构数据
* @param {String} childrenKey - 子节点的键名
* @returns {Array} 扁平化后的列表(包含层级和路径信息)
*/
function treeToListWithLevel(tree, childrenKey = 'children') {
const result = [];
function traverse(nodes, level = 0, path = []) {
nodes.forEach(node => {
const { [childrenKey]: children, ...item } = node;
// 添加层级和路径信息
const currentPath = [...path, item.id];
result.push({
...item,
level, // 层级深度
path: currentPath, // 从根到当前节点的路径
pathStr: currentPath.join('/') // 路径字符串
});
// 递归处理子节点
if (children && children.length > 0) {
traverse(children, level + 1, currentPath);
}
});
}
traverse(tree);
return result;
}
使用示例
// 示例数据:菜单树形结构
const menuTree = [
{
id: 1,
name: '系统管理',
path: '/system',
children: [
{
id: 11,
name: '用户管理',
path: '/system/user',
children: [
{ id: 111, name: '用户列表', path: '/system/user/list' },
{ id: 112, name: '添加用户', path: '/system/user/add' }
]
},
{
id: 12,
name: '角色管理',
path: '/system/role'
}
]
},
{
id: 2,
name: '内容管理',
path: '/content',
children: [
{ id: 21, name: '文章管理', path: '/content/article' },
{ id: 22, name: '分类管理', path: '/content/category' }
]
}
];
// 基础转换
const list1 = treeToList(menuTree);
console.log('基础转换:', list1);
// 输出: [
// { id: 1, name: '系统管理', path: '/system' },
// { id: 11, name: '用户管理', path: '/system/user', parentId: 1 },
// { id: 111, name: '用户列表', path: '/system/user/list', parentId: 11 },
// ...
// ]
// 迭代方式转换
const list2 = treeToListIterative(menuTree);
console.log('迭代转换:', list2);
// 保留层级信息
const list3 = treeToListWithLevel(menuTree);
console.log('带层级信息:', list3);
// 输出: [
// { id: 1, name: '系统管理', path: '/system', level: 0, path: [1], pathStr: '1' },
// { id: 11, name: '用户管理', path: '/system/user', level: 1, path: [1, 11], pathStr: '1/11' },
// ...
// ]
// 实际应用:获取所有菜单路径
const allPaths = treeToList(menuTree).map(item => item.path);
console.log('所有路径:', allPaths);
// 实际应用:查找特定菜单
const flatList = treeToList(menuTree);
const targetMenu = flatList.find(item => item.id === 111);
console.log('查找结果:', targetMenu);
关键点
-
递归 vs 迭代:递归实现代码简洁直观,迭代实现避免了深层嵌套可能导致的栈溢出问题
-
解构赋值:使用
const { children, ...item } = node优雅地分离子节点和其他属性 -
灵活的子节点键名:通过参数
childrenKey支持不同的数据结构(如children、subMenu等) -
保留关键信息:根据业务需求可以保留父节点 ID、层级深度、路径等信息,便于后续处理
-
性能考虑:对于大量数据,迭代方式更安全;递归方式更适合中小规模数据
-
边界处理:需要判断
children是否存在且为数组,避免空指针错误 -
不可变性:使用扩展运算符创建新对象,避免修改原始数据结构
目录