function arrayToTree(arr, parentId = null) {
const map = new Map();
const result = [];
// 第一次遍历:创建映射表
for (const item of arr) {
map.set(item.id, { ...item, children: [] });
}
// 第二次遍历:建立父子关系
for (const item of arr) {
const node = map.get(item.id);
if (item.parentId === parentId) {
// 根节点
result.push(node);
} else {
// 子节点
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(node);
}
}
}
return result;
}
// 测试数据
const flatArray = [
{ id: 1, name: '根节点1', parentId: null },
{ id: 2, name: '子节点1-1', parentId: 1 },
{ id: 3, name: '子节点1-2', parentId: 1 },
{ id: 4, name: '根节点2', parentId: null },
{ id: 5, name: '子节点1-1-1', parentId: 2 },
{ id: 6, name: '子节点2-1', parentId: 4 }
];
console.log(JSON.stringify(arrayToTree(flatArray), null, 2));
数组转树执行过程详解
📊 输入数据分析
JavaScript
const flatArray = [
{ id: 1, name: '根节点1', parentId: null },
{ id: 2, name: '子节点1-1', parentId: 1 },
{ id: 3, name: '子节点1-2', parentId: 1 },
{ id: 4, name: '根节点2', parentId: null },
{ id: 5, name: '子节点1-1-1', parentId: 2 },
{ id: 6, name: '子节点2-1', parentId: 4 }
];
数据结构分析:
- 2个根节点: id=1, id=4 (parentId为null)
- 4个子节点: id=2,3,5,6
- 层级关系: 1→2→5, 1→3, 4→6
🔄 第一次遍历:创建映射表
JavaScript
// 第一次遍历:创建映射表
for (const item of arr) {
map.set(item.id, { ...item, children: [] });
}
执行过程:
JavaScript
// 处理 { id: 1, name: '根节点1', parentId: null }
map.set(1, { id: 1, name: '根节点1', parentId: null, children: [] });
// 处理 { id: 2, name: '子节点1-1', parentId: 1 }
map.set(2, { id: 2, name: '子节点1-1', parentId: 1, children: [] });
// 处理 { id: 3, name: '子节点1-2', parentId: 1 }
map.set(3, { id: 3, name: '子节点1-2', parentId: 1, children: [] });
// 处理 { id: 4, name: '根节点2', parentId: null }
map.set(4, { id: 4, name: '根节点2', parentId: null, children: [] });
// 处理 { id: 5, name: '子节点1-1-1', parentId: 2 }
map.set(5, { id: 5, name: '子节点1-1-1', parentId: 2, children: [] });
// 处理 { id: 6, name: '子节点2-1', parentId: 4 }
map.set(6, { id: 6, name: '子节点2-1', parentId: 4, children: [] });
第一次遍历后的Map状态:
JavaScript
Map {
1 => { id: 1, name: '根节点1', parentId: null, children: [] },
2 => { id: 2, name: '子节点1-1', parentId: 1, children: [] },
3 => { id: 3, name: '子节点1-2', parentId: 1, children: [] },
4 => { id: 4, name: '根节点2', parentId: null, children: [] },
5 => { id: 5, name: '子节点1-1-1', parentId: 2, children: [] },
6 => { id: 6, name: '子节点2-1', parentId: 4, children: [] }
}
🌳 第二次遍历:建立父子关系
JavaScript
// 第二次遍历:建立父子关系
for (const item of arr) {
const node = map.get(item.id);
if (item.parentId === parentId) { // parentId = null
result.push(node);
} else {
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(node);
}
}
}
逐步执行过程:
步骤1: 处理 id=1
JavaScript
// item = { id: 1, name: '根节点1', parentId: null }
const node = map.get(1); // 获取节点1
// item.parentId (null) === parentId (null) ✅
result.push(node); // 添加到根节点数组
// result = [节点1]
步骤2: 处理 id=2
JavaScript
// item = { id: 2, name: '子节点1-1', parentId: 1 }
const node = map.get(2); // 获取节点2
// item.parentId (1) === parentId (null) ❌
const parent = map.get(1); // 获取父节点1
parent.children.push(node); // 节点2添加到节点1的children
// 节点1.children = [节点2]
步骤3: 处理 id=3
JavaScript
// item = { id: 3, name: '子节点1-2', parentId: 1 }
const node = map.get(3); // 获取节点3
// item.parentId (1) === parentId (null) ❌
const parent = map.get(1); // 获取父节点1
parent.children.push(node); // 节点3添加到节点1的children
// 节点1.children = [节点2, 节点3]
步骤4: 处理 id=4
JavaScript
// item = { id: 4, name: '根节点2', parentId: null }
const node = map.get(4); // 获取节点4
// item.parentId (null) === parentId (null) ✅
result.push(node); // 添加到根节点数组
// result = [节点1, 节点4]
步骤5: 处理 id=5
JavaScript
// item = { id: 5, name: '子节点1-1-1', parentId: 2 }
const node = map.get(5); // 获取节点5
// item.parentId (2) === parentId (null) ❌
const parent = map.get(2); // 获取父节点2
parent.children.push(node); // 节点5添加到节点2的children
// 节点2.children = [节点5]
步骤6: 处理 id=6
JavaScript
// item = { id: 6, name: '子节点2-1', parentId: 4 }
const node = map.get(6); // 获取节点6
// item.parentId (4) === parentId (null) ❌
const parent = map.get(4); // 获取父节点4
parent.children.push(node); // 节点6添加到节点4的children
// 节点4.children = [节点6]
🎯 最终结果
树形结构:
JavaScript
[
{
id: 1,
name: '根节点1',
parentId: null,
children: [
{
id: 2,
name: '子节点1-1',
parentId: 1,
children: [
{
id: 5,
name: '子节点1-1-1',
parentId: 2,
children: []
}
]
},
{
id: 3,
name: '子节点1-2',
parentId: 1,
children: []
}
]
},
{
id: 4,
name: '根节点2',
parentId: null,
children: [
{
id: 6,
name: '子节点2-1',
parentId: 4,
children: []
}
]
}
]
可视化树形结构:
根节点1 (id: 1)
├── 子节点1-1 (id: 2)
│ └── 子节点1-1-1 (id: 5)
└── 子节点1-2 (id: 3)
根节点2 (id: 4)
└── 子节点2-1 (id: 6)
📊 执行统计
处理统计:
- 总节点数: 6个
- 根节点数: 2个 (id: 1, 4)
- 子节点数: 4个 (id: 2, 3, 5, 6)
- 最大深度: 3层 (1→2→5)
Map操作统计:
- set操作: 6次 (第一次遍历)
- get操作: 10次 (第二次遍历中的node获取 + parent获取)
数组操作统计:
- result.push: 2次 (添加根节点)
- children.push: 4次 (添加子节点)
🔍 关键点总结
-
Map的作用: 提供O(1)的节点查找,避免嵌套循环
-
两次遍历的必要性:
- 第一次确保所有节点都存在于Map中
- 第二次建立引用关系时不会出现找不到父节点的情况
-
引用传递: Map中存储的是对象引用,修改children时直接影响最终结果
-
顺序无关性: 由于使用Map映射,原数组的顺序不影响最终树形结构
这就是整个数组转树的完整执行过程!
JavaScript 引用传递详解
🔍 核心概念
在 JavaScript 中,对象是通过引用传递的,这意味着变量存储的不是对象本身,而是指向对象在内存中位置的"地址"。
📊 内存模型图解
基本类型 vs 引用类型
JavaScript
// 基本类型 - 值传递
let a = 5;
let b = a; // 复制值
b = 10;
console.log(a); // 5 (不受影响)
// 引用类型 - 引用传递
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 复制引用地址
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob' (受影响!)
内存示意图
基本类型:
a → [内存地址1: 5]
b → [内存地址2: 10] // 独立的内存空间
引用类型:
obj1 → [引用地址] → [内存中的对象: {name: 'Bob'}]
obj2 → [相同引用地址] ↗ // 指向同一个对象
🌳 数组转树中的引用传递
关键代码分析
JavaScript
// 第一次遍历:创建映射表
for (const item of arr) {
map.set(item.id, { ...item, children: [] });
}
// 第二次遍历:建立父子关系
for (const item of arr) {
const node = map.get(item.id); // ← 获取对象引用
if (item.parentId === parentId) {
result.push(node); // ← 将引用添加到result
} else {
const parent = map.get(item.parentId); // ← 获取父节点引用
if (parent) {
parent.children.push(node); // ← 修改父节点的children
}
}
}
🔬 详细执行过程
步骤1: 创建对象并存储引用
JavaScript
// 假设处理 { id: 1, name: '根节点1', parentId: null }
const newObj = { id: 1, name: '根节点1', parentId: null, children: [] };
map.set(1, newObj);
// 内存状态:
// map: { 1 → [引用A] }
// [引用A] → { id: 1, name: '根节点1', parentId: null, children: [] }
步骤2: 获取引用并添加到result
JavaScript
const node = map.get(1); // node 获得 [引用A]
result.push(node); // result 中存储 [引用A]
// 内存状态:
// map: { 1 → [引用A] }
// result: [ [引用A] ]
// [引用A] → { id: 1, name: '根节点1', parentId: null, children: [] }
步骤3: 修改children属性
JavaScript
// 处理子节点 { id: 2, name: '子节点1-1', parentId: 1 }
const childNode = map.get(2); // childNode 获得 [引用B]
const parent = map.get(1); // parent 获得 [引用A] (同一个引用!)
parent.children.push(childNode); // 修改 [引用A] 指向的对象
// 内存状态:
// map: { 1 → [引用A], 2 → [引用B] }
// result: [ [引用A] ] // 注意:result中的引用A指向的对象已被修改!
// [引用A] → { id: 1, name: '根节点1', parentId: null, children: [[引用B]] }
// [引用B] → { id: 2, name: '子节点1-1', parentId: 1, children: [] }
🎯 实际演示代码
完整演示
JavaScript
function demonstrateReference() {
const map = new Map();
const result = [];
// 创建对象
const obj1 = { id: 1, name: '节点1', children: [] };
const obj2 = { id: 2, name: '节点2', children: [] };
// 存储到Map
map.set(1, obj1);
map.set(2, obj2);
console.log('=== 初始状态 ===');
console.log('obj1:', obj1);
console.log('map.get(1):', map.get(1));
console.log('obj1 === map.get(1):', obj1 === map.get(1)); // true - 同一个引用
// 添加到result
result.push(map.get(1));
console.log('\n=== 添加到result后 ===');
console.log('result[0] === obj1:', result[0] === obj1); // true - 同一个引用
console.log('result[0] === map.get(1):', result[0] === map.get(1)); // true
// 修改children
const parent = map.get(1);
const child = map.get(2);
parent.children.push(child);
console.log('\n=== 修改children后 ===');
console.log('obj1.children:', obj1.children); // 已被修改
console.log('result[0].children:', result[0].children); // 同样被修改
console.log('map.get(1).children:', map.get(1).children); // 同样被修改
console.log('\n=== 验证引用关系 ===');
console.log('所有引用指向同一对象:',
obj1 === result[0] &&
result[0] === map.get(1) &&
map.get(1) === parent
); // true
}
demonstrateReference();
输出结果
=== 初始状态 ===
obj1: { id: 1, name: '节点1', children: [] }
map.get(1): { id: 1, name: '节点1', children: [] }
obj1 === map.get(1): true
=== 添加到result后 ===
result[0] === obj1: true
result[0] === map.get(1): true
=== 修改children后 ===
obj1.children: [ { id: 2, name: '节点2', children: [] } ]
result[0].children: [ { id: 2, name: '节点2', children: [] } ]
map.get(1).children: [ { id: 2, name: '节点2', children: [] } ]
=== 验证引用关系 ===
所有引用指向同一对象: true
🔄 引用传递的优势
1. 内存效率
JavaScript
// 不需要复制整个对象,只传递引用
const largeObject = { /* 包含大量数据 */ };
const reference1 = largeObject; // 只复制引用,不复制数据
const reference2 = largeObject; // 只复制引用,不复制数据
2. 实时同步
JavaScript
function arrayToTree(arr) {
const map = new Map();
const result = [];
// 所有变量都指向同一个对象实例
for (const item of arr) {
const node = { ...item, children: [] };
map.set(item.id, node);
if (!item.parentId) {
result.push(node); // result中存储引用
}
}
for (const item of arr) {
if (item.parentId) {
const parent = map.get(item.parentId); // 获取同一引用
const child = map.get(item.id); // 获取同一引用
parent.children.push(child); // 修改会影响result中的对象
}
}
return result; // 返回的对象已经包含了所有修改
}
⚠️ 注意事项和陷阱
1. 意外修改
JavaScript
const tree = arrayToTree(data);
const firstNode = tree[0];
// 危险!修改会影响原树结构
firstNode.name = '修改后的名称';
console.log(tree[0].name); // '修改后的名称' - 被意外修改
2. 深拷贝需求
JavaScript
// 如果需要独立的副本
function getIndependentCopy(tree) {
return JSON.parse(JSON.stringify(tree)); // 深拷贝
}
const independentTree = getIndependentCopy(originalTree);
independentTree[0].name = '修改'; // 不会影响原树
3. 循环引用问题
JavaScript
// 如果存在循环引用
const obj = { name: 'test' };
obj.self = obj; // 循环引用
// JSON.stringify(obj); // 报错:Converting circular structure to JSON
📚 总结
在数组转树的算法中,引用传递的关键作用:
- Map存储引用:
map.set(id, nodeObject)存储的是对象引用 - result存储引用:
result.push(node)添加的是同一个引用 - 修改同步: 通过
parent.children.push(child)修改对象时,所有引用该对象的地方都会看到变化 - 内存效率: 避免了对象的重复创建和复制
这就是为什么我们可以在第二次遍历中修改 parent.children,而最终返回的 result 数组中的对象会自动包含这些修改的原因!