嵌套分类数据通常以树形结构存储,每个节点可能包含自身的属性信息和子节点数组,实际开发中经常需要从中提取符合特定条件的子节点,并将这些子节点展平为单一的列表结构,方便进行后续的数据渲染、统计或者接口返回。

嵌套分类数据的常见结构
典型的嵌套分类数据格式如下,每个节点包含id、name、type等属性,同时可能有children字段存储子节点:
// 示例嵌套分类数据
const categoryData = [
{
id: 1,
name: '数码产品',
type: 'parent',
children: [
{
id: 11,
name: '手机',
type: 'child',
children: [
{ id: 111, name: '智能手机', type: 'leaf' },
{ id: 112, name: '功能手机', type: 'leaf' }
]
},
{
id: 12,
name: '电脑',
type: 'child',
children: [
{ id: 121, name: '笔记本电脑', type: 'leaf' },
{ id: 122, name: '台式电脑', type: 'leaf' }
]
}
]
},
{
id: 2,
name: '家居用品',
type: 'parent',
children: [
{ id: 21, name: '家具', type: 'leaf' },
{ id: 22, name: '家纺', type: 'leaf' }
]
}
];
递归方式提取并展平子节点
递归是最直观的处理树形结构的方式,我们可以定义一个递归函数,遍历每个节点,判断是否符合提取条件,符合则加入结果数组,再继续遍历其子节点。
假设我们需要提取所有type为leaf的子节点,实现代码如下:
/**
* 递归提取嵌套分类数据中的指定子节点并展平
* @param {Array} data 嵌套分类数据
* @param {Function} filterFn 筛选条件函数,返回true表示需要提取该节点
* @returns {Array} 展平后的节点数组
*/
function flattenSpecifiedNodes(data, filterFn) {
const result = [];
// 递归遍历函数
function traverse(nodes) {
if (!Array.isArray(nodes)) return;
nodes.forEach(node => {
// 判断当前节点是否符合筛选条件
if (filterFn(node)) {
result.push(node);
}
// 如果存在子节点,继续递归遍历
if (node.children && node.children.length > 0) {
traverse(node.children);
}
});
}
traverse(data);
return result;
}
// 调用函数提取type为leaf的节点
const leafNodes = flattenSpecifiedNodes(categoryData, node => node.type === 'leaf');
console.log(leafNodes);
// 输出结果包含111、112、121、122、21、22共6个叶子节点
迭代方式实现节点提取与展平
递归方式在嵌套层级过深时可能出现栈溢出问题,此时可以使用迭代方式,通过栈或队列来维护待遍历的节点。
同样以提取type为leaf的节点为例,迭代实现代码如下:
/**
* 迭代方式提取嵌套分类数据中的指定子节点并展平
* @param {Array} data 嵌套分类数据
* @param {Function} filterFn 筛选条件函数
* @returns {Array} 展平后的节点数组
*/
function flattenSpecifiedNodesIteratively(data, filterFn) {
const result = [];
// 使用栈存储待处理的节点,初始放入所有根节点
const stack = [...data];
while (stack.length > 0) {
// 取出栈顶节点
const currentNode = stack.pop();
// 判断是否符合筛选条件
if (filterFn(currentNode)) {
result.push(currentNode);
}
// 如果存在子节点,将子节点放入栈中待处理
if (currentNode.children && Array.isArray(currentNode.children)) {
// 使用扩展运算符将子节点加入栈,也可以用push循环加入
stack.push(...currentNode.children);
}
}
return result;
}
// 调用迭代函数提取叶子节点
const leafNodesIterative = flattenSpecifiedNodesIteratively(categoryData, node => node.type === 'leaf');
console.log(leafNodesIterative);
两种方式的对比
我们可以通过表格对比两种实现方式的特点:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 递归方式 | 代码逻辑直观,符合树形结构的遍历思维,实现简单 | 嵌套层级过深时可能导致调用栈溢出 | 嵌套层级较浅,数据量不大的场景 |
| 迭代方式 | 没有栈溢出风险,性能更稳定 | 代码逻辑相对复杂,需要手动维护遍历队列 | 嵌套层级深,数据量大的场景 |
实际场景中的扩展用法
上述示例的筛选条件是固定的,实际开发中我们可以灵活调整filterFn函数来实现不同的提取需求:
- 提取指定id的子节点:
node => node.id === 12 - 提取名称包含特定关键词的节点:
node => node.name.includes('手机') - 提取所有非叶子节点:
node => node.type !== 'leaf'
如果需要提取的子节点还要保留部分父节点信息,可以在筛选时给节点添加额外的属性,再推入结果数组,比如记录父节点名称:
function flattenWithParentInfo(data, filterFn) {
const result = [];
function traverse(nodes, parentName = '') {
nodes.forEach(node => {
// 给符合条件的节点添加父节点名称属性
if (filterFn(node)) {
const newNode = { ...node, parentName };
result.push(newNode);
}
// 递归子节点时传入当前节点名称作为父节点名称
if (node.children) {
traverse(node.children, node.name);
}
});
}
traverse(data);
return result;
}
// 提取叶子节点并附带父节点名称
const leafWithParent = flattenWithParentInfo(categoryData, node => node.type === 'leaf');
console.log(leafWithParent[0].parentName); // 输出 手机
在处理嵌套分类数据时,根据实际的数据规模和嵌套深度选择合适的遍历方式,就能高效完成指定子节点的提取和展平操作。