
引言
在前端开发或数据处理中,我们经常会遇到需要将扁平化的数据结构转换为具有层级关系的树形结构。例如,一个导航菜单、一个评论列表或者一个文件目录,它们在数据库中可能以扁平列表的形式存储,但展示时需要呈现出清晰的父子嵌套关系。本教程将指导您如何利用javascript,将一个包含 level 字段的扁平json数组,转换为一个以 subnav 属性表示子节点的嵌套json结构。
问题分析
假设我们有以下扁平的JSON数据,其中每个对象都包含一个 title 和 metaData,metaData 中有一个 level 字段,表示该项在层级结构中的深度:
const content = [
{ title: "Item 1", metaData: { "level": 1, "desc": "Some Desc 1", "displayOnOverview": true }},
{ title: "Item 2", metaData: { "level": 2, "desc": "Some Desc 2", "displayOnOverview": true }},
{ title: "Item 3", metaData: { "level": 2, "desc": "Some Desc 3", "displayOnOverview": false }},
{ title: "Item 4", metaData: { "level": 3, "desc": "Some Desc 4", "displayOnOverview": true }},
{ title: "Item 5", metaData: { "level": 1, "desc": "Some Desc 5", "displayOnOverview": true }}
];我们期望的输出是一个嵌套的JSON结构,其中 level: 1 的项是顶级节点,level: N 的项是 level: N-1 的项的子节点,并通过 subNav 数组来表示:
[
{
"title": "Item 1",
"metaData": {
"level": 1,
"desc": "Some Desc 1",
"displayOnOverview": true
},
"subNav": [
{
"title": "Item 2",
"metaData": {
"level": 2,
"desc": "Some Desc 2",
"displayOnOverview": true
}
},
{
"title": "Item 3",
"metaData": {
"level": 2,
"desc": "Some Desc 3",
"displayOnOverview": false
},
"subNav": [
{
"title": "Item 4",
"metaData": {
"level": 3,
"desc": "Some Desc 4",
"displayOnOverview": true
}
}
]
}
]
},
{
"title": "Item 5",
"metaData": {
"level": 1,
"desc": "Some Desc 5",
"displayOnOverview": true
}
}
]核心挑战在于,在遍历扁平数据时,如何准确地找到当前节点的父节点,并将其添加到正确的 subNav 数组中。简单的基于索引的判断(如 root[index - 1])在处理多级嵌套时会失效,因为它无法正确追踪到不同层级的父节点。
核心思路
解决这个问题的关键在于维护一个机制,能够动态地记录每个层级的“当前父节点”。当遍历到一个新节点时,我们可以根据其 level 值,向上追溯到 level - 1 的父节点。
我们可以使用一个映射(Map 或普通JavaScript对象)来实现这一目标。这个映射将以 level 作为键,以该层级的 最新处理过的节点 作为值。这样,当我们需要查找 level N 的父节点时,只需从映射中取出 level N-1 对应的节点即可。
实现步骤与代码
下面是实现这一转换的JavaScript函数:
const content = [
{ title: "Item 1", metaData: { "level": 1, "desc": "Some Desc 1", "displayOnOverview": true }},
{ title: "Item 2", metaData: { "level": 2, "desc": "Some Desc 2", "displayOnOverview": true }},
{ title: "Item 3", metaData: { "level": 2, "desc": "Some Desc 3", "displayOnOverview": false }},
{ title: "Item 4", metaData: { "level": 3, "desc": "Some Desc 4", "displayOnOverview": true }},
{ title: "Item 5", metaData: { "level": 1, "desc": "Some Desc 5", "displayOnOverview": true }}
];
/**
* 将扁平JSON数组转换为嵌套层级结构
* @param {Array代码解析
- topLevelItems 数组: 这个数组用于收集所有 level 为 1 的节点,它们将构成最终输出的根级别元素。
- itemMap 对象: 这是实现层级追踪的核心。它是一个简单的JavaScript对象,键是 level 数字(例如 1, 2, 3),值是当前遍历过程中 最新遇到的 属于该 level 的节点对象。
- 遍历 data: 我们使用 for...of 循环遍历输入的扁平数据数组 data。
- 创建 newItem: 为了不直接修改原始数据,我们创建了一个 newItem 的浅拷贝。如果 metaData 内部也可能被修改,或者包含更深层次的对象,您可能需要进行深拷贝。
-
判断 level:
- currentLevel === 1: 如果当前节点的 level 是 1,它就是一个顶级节点,直接将其添加到 topLevelItems 数组中。
- currentLevel > 1: 如果 level 大于 1,则它是一个子节点。我们通过 parentLevel = currentLevel - 1 计算出其父节点的层级。
- 查找父节点: 从 itemMap[parentLevel] 中获取对应的父节点。由于 itemMap 总是存储每个层级的最新节点,因此这里能正确找到当前节点的直接父级。
- 初始化 subNav: 如果找到的 parentItem 还没有 subNav 属性,说明这是它第一次接收子节点,需要将其初始化为一个空数组。
- 添加子节点: 将 newItem 添加到 parentItem.subNav 数组中。
-
更新 itemMap: 最关键的一步。在处理完 newItem 后,无论它是顶级节点还是子节点,我们都将其存储到 itemMap[currentLevel] 中。这样做是为了确保:
- 如果后续有同 level 的节点出现,itemMap 将被更新为最新的节点,保证其子节点能正确找到它。
- 如果后续有 level + 1 的节点出现,它能通过 itemMap[level] 找到 newItem 作为其父节点。
注意事项
-
数据完整性与顺序:
- 该算法假定输入数据是按层级顺序组织的,即父节点总是在其子节点之前出现。如果数据顺序混乱,可能导致父节点未被 itemMap 记录就尝试查找,从而出现错误。
- level 值应是连续且有效的。例如,不应出现 level: 3 的节点,而 itemMap 中没有 level: 2 的父节点记录。如果存在此类情况,代码中的 console.warn 会提示,您可能需要更健壮的错误处理机制。
- 深拷贝与浅拷贝: 示例代码中使用了 const newItem = { ...item }; 进行浅拷贝。这意味着 newItem.metaData 仍然指向原始 item.metaData 对象的引用。如果您的 metaData 字段可能会在嵌套过程中被修改,并且您不希望影响原始数据,则需要对 metaData 进行深拷贝。
- 性能: 这种基于单次遍历和哈希表(itemMap)的解决方案,其时间复杂度为 O(N),其中 N 是输入数据数组的长度。对于大多数应用场景,这是一种高效的解决方案。
- 通用性: 该方法非常通用,可以处理任意深度的嵌套结构,只要 level 字段能够正确指示层级关系即可。
总结
通过巧妙地利用一个 itemMap 来动态追踪每个层级的最新节点,我们可以高效且准确地将扁平化的JSON数组转换为具有父子关系的嵌套结构。这种模式在处理树形数据、构建导航菜单、组织文件结构等场景中非常实用,提供了一种清晰、可维护且高性能的解决方案。理解 itemMap 的作用及其在循环中的更新机制,是掌握此转换方法的关键。










