如何用JavaScript将嵌套JSON数组扁平化
在前端开发中,我们经常会遇到结构嵌套的JSON数组数据,例如树形菜单、多级分类等场景。这类数据的嵌套层级不固定,直接处理时往往需要编写复杂的递归逻辑。将嵌套JSON数组扁平化,就是把多层嵌套的结构转换为只有一层的线性数组,方便后续的数据遍历、筛选和渲染。
嵌套JSON数组的典型结构
假设我们有如下嵌套的JSON数组,每个对象包含自身信息和子节点数组:
const nestedData = [
{
id: 1,
name: '一级分类A',
children: [
{
id: 2,
name: '二级分类A1',
children: [
{ id: 3, name: '三级分类A1-1', children: [] },
{ id: 4, name: '三级分类A1-2', children: [] }
]
},
{
id: 5,
name: '二级分类A2',
children: []
}
]
},
{
id: 6,
name: '一级分类B',
children: [
{ id: 7, name: '二级分类B1', children: [] }
]
}
];扁平化的核心实现思路
扁平化的核心逻辑是遍历嵌套数组,将当前层级的对象提取出来,再递归处理其子节点(如果存在)。常见的实现方式有两种:递归遍历法和栈迭代法。
方法一:递归遍历法
递归是最直观的实现方式,逻辑清晰,适合嵌套层级不特别深的场景。步骤为:
定义一个结果数组,用于存储扁平化后的数据
遍历输入数组的每个元素,将当前元素(可选择性去除子节点属性)存入结果数组
如果当前元素存在子节点数组且长度大于0,递归调用扁平化函数处理子节点
最终返回结果数组
实现代码如下:
function flattenJsonArray(data, childrenKey = 'children') {
const result = [];
function traverse(arr) {
arr.forEach(item => {
// 复制当前对象,避免修改原数据
const newItem = { ...item };
// 如果不需要保留原数据中的children属性,可以删除
// delete newItem[childrenKey];
result.push(newItem);
// 如果存在子节点,递归处理
if (newItem[childrenKey] && Array.isArray(newItem[childrenKey]) && newItem[childrenKey].length > 0) {
traverse(newItem[childrenKey]);
}
});
}
traverse(data);
return result;
}
// 调用测试
const flatData = flattenJsonArray(nestedData);
console.log(flatData);上述代码中,我们通过traverse函数递归遍历所有节点,将所有对象按顺序存入result数组。如果需要去掉扁平化后数据中的children属性,可以取消delete newItem[childrenKey];的注释。
方法二:栈迭代法
递归法在嵌套层级过深时可能出现栈溢出问题,栈迭代法通过手动维护栈结构来避免这个问题,适合处理深度嵌套的数据。步骤为:
将初始数据数组放入栈中
循环判断栈是否为空,不为空则取出栈顶元素
将当前元素存入结果数组,将其子节点数组逆序推入栈中(保证遍历顺序和递归一致)
重复上述步骤直到栈为空
实现代码如下:
function flattenJsonArrayByStack(data, childrenKey = 'children') {
const result = [];
// 初始化栈,将初始数据放入
const stack = [...data];
while (stack.length > 0) {
const current = stack.pop();
// 复制当前对象,避免修改原数据
const newItem = { ...current };
result.push(newItem);
// 如果存在子节点,逆序推入栈中,保证遍历顺序和递归一致
if (newItem[childrenKey] && Array.isArray(newItem[childrenKey]) && newItem[childrenKey].length > 0) {
// 逆序推入,因为栈是后进先出
stack.push(...newItem[childrenKey].reverse());
}
}
return result;
}
// 调用测试
const flatDataByStack = flattenJsonArrayByStack(nestedData);
console.log(flatDataByStack);两种方法的对比
可以通过以下表格对比两种实现方案的特点:
| 对比维度 | 递归遍历法 | 栈迭代法 |
|---|---|---|
| 代码可读性 | 逻辑直观,容易理解 | 需要理解栈的工作原理,可读性稍弱 |
| 嵌套深度限制 | 受调用栈深度限制,过深可能栈溢出 | 无调用栈限制,适合深度嵌套数据 |
| 遍历顺序 | 默认深度优先遍历 | 可调整栈的入栈顺序控制遍历顺序 |
实际场景中的扩展处理
在实际业务中,我们可能还需要给扁平化后的数据添加层级标识,方便后续区分不同层级的数据。例如给每个节点添加level字段表示其所在层级:
function flattenJsonArrayWithLevel(data, childrenKey = 'children', currentLevel = 1) {
const result = [];
function traverse(arr, level) {
arr.forEach(item => {
const newItem = { ...item, level };
result.push(newItem);
if (newItem[childrenKey] && Array.isArray(newItem[childrenKey]) && newItem[childrenKey].length > 0) {
traverse(newItem[childrenKey], level + 1);
}
});
}
traverse(data, currentLevel);
return result;
}
// 调用测试,一级节点level为1,二级为2,以此类推
const flatDataWithLevel = flattenJsonArrayWithLevel(nestedData);
console.log(flatDataWithLevel);通过这种方式,扁平化后的数据不仅保留了原有信息,还增加了层级标识,更适合用于树形结构的展开折叠、缩进渲染等场景。
注意事项
在处理嵌套JSON数组时,需要注意以下几点:
避免直接修改原数据,建议通过展开运算符
...或者Object.assign复制对象后再处理如果子节点属性名不是默认的
children,可以通过参数传入自定义的属性名,提高函数通用性如果数据中存在循环引用(例如子节点引用父节点),需要额外添加已访问对象的判断,避免死循环
通过上述方法,我们可以快速实现嵌套JSON数组的扁平化,根据实际的嵌套深度和业务需求选择合适的实现方案即可。