在处理多层嵌套的数组结构时,我们需要从中筛选出符合特定类型的对象,传统的递归遍历方式存在栈溢出风险,基于栈的迭代遍历策略是更优的解决方案。

为什么需要栈迭代遍历策略
复杂嵌套数组指的是数组元素中既包含普通值,又包含子数组,子数组内还可能继续嵌套数组的结构,比如接口返回的多级菜单数据、树形结构扁平化前的原始数据等。如果我们要从中提取所有类型为对象的元素,或者符合某个属性条件的对象,常见的问题有两个:
- 递归遍历的深度受限于JavaScript引擎的调用栈大小,嵌套层级过深时会直接抛出栈溢出错误
- 递归函数的上下文切换会带来额外的性能开销,处理大量数据时效率偏低
基于栈的迭代遍历策略通过手动维护一个栈容器,模拟递归的遍历过程,完全避免了函数调用栈的限制,同时遍历逻辑更可控,执行效率更高。
栈迭代遍历的核心原理
栈是一种后进先出的数据结构,基于栈的遍历核心逻辑和递归的前序遍历一致,只是把递归隐式的调用栈换成了显式的数组栈:
- 初始化一个栈,把原始嵌套数组的所有元素依次压入栈中
- 循环判断栈是否为空,不为空则弹出栈顶元素
- 判断弹出的元素是否符合目标类型要求,符合则加入结果集合
- 如果弹出的元素是数组,就把该数组的所有元素依次压入栈中,继续下一轮循环
- 直到栈为空,遍历结束,返回结果集合
具体实现代码示例
以下是一个通用的从嵌套数组中提取指定类型对象的实现,支持自定义类型判断条件:
/**
* 从嵌套数组中提取符合指定条件的对象
* @param {Array} nestedArr 原始嵌套数组
* @param {Function} condition 判断条件函数,接收元素返回布尔值
* @returns {Array} 符合条件的对象集合
*/
function extractTargetObjects(nestedArr, condition) {
// 初始化结果数组和栈,栈初始放入原始数组的所有元素
const result = [];
const stack = [...nestedArr];
// 栈不为空就持续遍历
while (stack.length > 0) {
// 弹出栈顶元素
const current = stack.pop();
// 判断当前元素是否符合条件,符合则加入结果
if (condition(current)) {
result.push(current);
}
// 如果当前元素是数组,就把所有子元素压入栈中
if (Array.isArray(current)) {
// 用push把子元素加入栈,保证遍历顺序和前序递归一致
stack.push(...current);
}
}
return result;
}
// 测试用例:复杂嵌套数组
const testData = [
1,
'string',
{ id: 1, name: '对象1' },
[
{ id: 2, name: '对象2' },
2,
[
{ id: 3, name: '对象3' },
[ { id: 4, name: '对象4' } ]
]
],
null
];
// 提取所有类型为对象的元素
const targetObjects = extractTargetObjects(testData, (item) => typeof item === 'object' && item !== null && !Array.isArray(item));
console.log(targetObjects);
// 输出:[{ id: 1, name: '对象1' }, { id: 2, name: '对象2' }, { id: 3, name: '对象3' }, { id: 4, name: '对象4' }]
策略优化与注意事项
实际使用中可以根据需求对基础实现做优化:
- 如果不需要保持遍历顺序,弹出元素时可以用
pop,如果需要前序遍历的顺序,可以用shift取栈底元素,不过shift的时间复杂度更高,数据量大时建议还是用pop配合反向压栈 - 如果嵌套数组中存在循环引用(比如子元素引用了父数组),需要额外维护一个访问过的元素集合,避免无限循环,判断元素是否在集合中,在则跳过
- 条件函数可以灵活定制,比如提取包含特定属性名的对象、属性值符合要求的对象等,不需要修改遍历核心逻辑
适用场景总结
这种基于栈的迭代遍历策略适合所有需要处理不确定深度嵌套数组的场景,尤其是嵌套层级可能超过100层、或者数据量极大的情况,相比递归遍历稳定性更高,性能也更好。如果嵌套层级固定且很浅,递归写法更简洁,也可以根据实际场景选择。