用 reduce 构建菜单树的核心是通过一次遍历,利用 map 缓存节点引用并动态挂载 children,而非依赖递归;先确保每个节点在 map 中存在且 children 初始化,再按 parentId 挂载到父节点,最后提取根节点。

用 reduce 构建菜单树的核心,是把扁平数组中每个节点按 id 和 parentId 关系“挂载”到对应父节点的 children 数组里,同时维护一个快速查找的映射表。关键不在递归,而在一次遍历中完成引用关系的建立。
准备数据结构:带 parentId 的扁平列表
菜单数据通常是后端返回的扁平数组,每个项至少包含:id、parentId(根节点一般为 null 或 0)、name 等字段。例如:
[
{ id: 1, parentId: null, name: "首页" },
{ id: 2, parentId: null, name: "产品" },
{ id: 3, parentId: 2, name: "Web 应用" },
{ id: 4, parentId: 2, name: "移动端" },
{ id: 5, parentId: 3, name: "控制台" }
]
用 reduce 建立 id → 节点引用映射
不直接拼树,而是先构建一个以 id 为键的对象,存储每个节点的引用(含初始化 children: [])。这样后续挂载子节点时能 O(1) 找到父节点。
- 初始化一个空对象
map = {},作为映射容器 - 对每个节点,先确保它在 map 中存在(避免重复创建),并初始化
children数组 - 如果
parentId存在且不为空,就把它 push 到对应父节点的children中 - 最后过滤出所有
parentId为空的节点,就是根节点
完整实现代码(带注释)
以下是一个健壮、可读性强的实现:
function buildMenuTree(list) {
const map = {};
// 第一遍:创建所有节点引用,并初始化 children
list.forEach(item => {
map[item.id] = { ...item, children: map[item.id]?.children || [] };
});
// 第二遍:挂载子节点(也可合并进上面 forEach,但分开展示逻辑更清晰)
list.forEach(item => {
if (item.parentId != null && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 收集所有根节点(parentId 为 null / undefined / 0 等,按实际业务调整)
return list.filter(item => item.parentId == null).map(item => map[item.id]);
}
注意:item.parentId == null 使用宽松比较,兼容 null 和 undefined;若后端用 0 表示根,请改为 item.parentId === 0 || item.parentId == null。
优化:单次 reduce 完成全部逻辑
如果坚持“纯 reduce”风格,可以合并两步操作。核心是:每次迭代都确保当前节点已存在,并尝试将其加入父节点的 children —— 即使父节点还没处理完,后续也会被补上。
function buildMenuTreeReduce(list) {
const map = {};
list.reduce((acc, item) => {
// 确保当前节点存在(含 children 初始化)
if (!map[item.id]) {
map[item.id] = { ...item, children: [] };
}
// 如果有父节点,尝试挂载(父节点可能尚未创建,但没关系,后面会轮到它)
if (item.parentId != null && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
// 根节点加入 acc,用于最后返回
if (item.parentId == null) {
acc.push(map[item.id]);
}
return acc;
}, []);
return acc;
}
这个版本真正只用了一次 reduce,且无需额外遍历。只要数据中父子关系一致(即父节点一定出现在子节点之前,或即使顺序乱也能靠 map 缓存兜底),就能正确生成树。
不复杂但容易忽略:关键是理解 reduce 在这里不是做累加,而是做“状态累积”——map 是闭包中的可变状态,reduce 只是驱动遍历的语法糖。真正的树结构,靠的是对象引用和 children 数组的动态 push。

















