JavaScript数组扁平化处理的3种代码实例
深入讲解JavaScript数组扁平化的核心原理与应用场景,提供ES6原生flat、递归reduce与基于栈的迭代法3种完整代码实例与选型对比。
假设你正在对接后端返回的组织架构或级联选择器数据,拿到的是形如 [1, [2, [3, 4], 5]] 的多层嵌套数组。而在页面渲染、全局搜索或数据统计时,程序往往只需要一个纯粹的一维列表 [1, 2, 3, 4, 5]。这种将多维嵌套结构降解为单一维度的操作,在编程中被称为“数组扁平化”(Array Flattening)。本文将通过由浅入深的3种典型代码实例,拆解原生语法、递归累加与迭代防爆栈的具体实现与适用场景。
一、什么是数组扁平化
在 JavaScript 中,数组允许存放任意类型的元素,包含另一个数组。当数组内部包含子数组,子数组内部又嵌套深层数组时,就形成了“树状”的嵌套层次。扁平化处理的核心目标,是提取出所有非数组的叶子节点元素,按照原始出现顺序重新组合成平级的一维数组。
二、实例一:ES6 原生 flat() 方法
ECMAScript 2019 (ES10) 正式为数组原型引入了 Array.prototype.flat() 方法。这是现代浏览器和 Node.js 环境中最为推崇、语法最简洁的原生方案。
1. 基础用法与深度控制
flat() 方法接收一个可选的数值参数 depth,用于指定需要拉平的嵌套层数,默认值为 1。如果需要无视嵌套层级、彻底将数组拉平成一维,可以传入 Infinity 关键字。
// 示例 1:使用默认深度(depth = 1)
const nestedArray = [1, [2, [3, 4], 5]];
const flatLevel1 = nestedArray.flat();
console.log(flatLevel1);
// 示例输出: [1, 2, [3, 4], 5] (仅展开了最外层子数组)
// 示例 2:传入特定深度 depth = 2
const flatLevel2 = nestedArray.flat(2);
console.log(flatLevel2);
// 示例输出: [1, 2, 3, 4, 5]
// 示例 3:传入 Infinity 彻底扁平化任意深度的嵌套
const deepNested = [1, [2, [3, [4, [5]]]]];
const completelyFlat = deepNested.flat(Infinity);
console.log(completelyFlat);
// 示例输出: [1, 2, 3, 4, 5]
flat() 在执行降维的同时,会自动移除稀疏数组中的空位(empty slots)。例如 [1, 2, , 4, [5]].flat() 执行后输出为 [1, 2, 4, 5],空位不会转换为 undefined,而是被直接跳过。
三、实例二:基于 reduce 与 concat 的递归实现
在需要向下兼容较早的执行环境(例如旧版移动端 WebView 或旧版 Node.js),或者需要深度定制扁平化过滤规则时,手动实现递归函数是最常见、也是最经典的做法。
1. 完整代码实现
通过数组的 reduce 方法遍历每一项,若遇到子数组则进行自调用递归,否则将普通元素拼接到累加器 acc 中:
/**
* 递归扁平化函数
* @param {Array} arr 需要处理的数组
* @param {number} depth 展开深度,默认 1 层
* @returns {Array} 扁平化后的新数组
*/
function flattenRecursive(arr, depth = 1) {
// 边界保护:非数组或展开深度耗尽时,直接返回原数组的浅拷贝
if (!Array.isArray(arr) || depth <= 0) {
return Array.isArray(arr) ? arr.slice() : [arr];
}
return arr.reduce((acc, current) => {
if (Array.isArray(current)) {
// 若元素为数组,递归解构下一层并连接
return acc.concat(flattenRecursive(current, depth - 1));
} else {
// 普通项直接追加
return acc.concat(current);
}
}, []);
}
// 测试执行
const data = ["a", ["b", ["c", "d"]], "e"];
console.log(flattenRecursive(data, 1));
// 示例输出: ["a", "b", ["c", "d"], "e"]
console.log(flattenRecursive(data, Infinity));
// 示例输出: ["a", "b", "c", "d", "e"]
2. 代码要点解析
- 累积拼接:使用
concat代替直接push,确保不会污染外部传入的原始数据引用,符合不可变数据(Immutability)的函数式编程规范。 - 递归终止条件:通过
depth <= 0控制递归深度。当层级耗尽时,直接返回arr.slice(),防止无限制递归。 - 隐患排查:该方案在嵌套层数较低时直观优雅,但由于 JavaScript 引擎的调用栈(Call Stack)深度通常有限(常见约为 10000 帧左右),如果遇到恶意构造的极端深层嵌套,可能会触发
RangeError: Maximum call stack size exceeded异常。
四、实例三:基于栈(Stack)的迭代循环实现
针对深层嵌套带来的调用栈溢出风险,我们可以借助自主维护的数据结构——在堆内存中模拟“栈(Stack)”,使用 while 循环将递归转化为迭代。由于堆内存空间远大于执行栈,这种方案具备极强的防爆栈能力。
1. 完整代码实现
/**
* 基于栈结构的迭代扁平化(彻底展开所有嵌套)
* @param {Array} arr 待处理的多维数组
* @returns {Array} 完全扁平化后的一维数组
*/
function flattenIterative(arr) {
if (!Array.isArray(arr)) return [];
// 浅拷贝原数组至工作栈中,避免直接篡改输入源
const stack = [...arr];
const result = [];
while (stack.length > 0) {
// 弹出栈顶元素
const next = stack.pop();
if (Array.isArray(next)) {
// 如果是子数组,使用展开运算符解构成独立元素重新压栈
stack.push(...next);
} else {
// 非数组元素存入结果收集池
result.push(next);
}
}
// 由于是从栈顶(末尾)开始弹出的,最终结果需要反转以恢复原顺序
return result.reverse();
}
// 测试执行
const deepList = [10, [20, [30, 40], 50], 60];
console.log(flattenIterative(deepList));
// 示例输出: [10, 20, 30, 40, 50, 60]
五、3种方案对比与选型建议
在日常开发与项目重构中,面对不同体量的数据和运行环境,合理选型能够兼顾执行效率与系统健壮性:
| 方案名称 | 代码复杂度 | 深度控制 | 极限深度抗溢出能力 | 适用场景 |
|---|---|---|---|---|
| 1. 原生 flat() | 极低(一行代码) | 完美(传参数值或 Infinity) | 良好(底层 C++ 引擎优化) | 现代浏览器、主流 Node.js 后端开发(首选) |
| 2. 递归 reduce | 中等(易于理解扩展) | 灵活(代码内按需调整) | 受限(深度超过上万层可能爆栈) | 低版本兼容环境、需要定制数据转换的管道 |
| 3. 迭代 Stack | 中等(涉及栈操作与翻转) | 需额外计数器控制 | 极佳(仅受限于堆内存大小) | 处理超大未知深度的层级数据、算法高频处理 |
六、总结与实践注意事项
在工程实践中,除了掌握以上三种实现方式,还需要警惕以下两个常见边界问题:
- 循环引用(Circular Reference):若嵌套数组中某个子项引用了其父级数组本身(例如
const a = []; a.push(a);),上述递归和简单迭代都会陷入死循环。对于可能存在环状引用的复杂数据,应配合WeakSet或Set记录已经遍历过的数组引用。 - 浅拷贝引用效应:扁平化操作处理的是嵌套层级,如果数组中包含的是对象字面量(如
[{ id: 1 }, [{ id: 2 }]]),扁平化提取出的对象依然指向原本的对象内存地址。若业务需要完全隔离,应在扁平化后或处理过程中结合structuredClone()进行深拷贝。
通常情况下,直接使用 arr.flat(Infinity) 能够满足 95% 以上的日常开发需求;而在面临兼容性降级或对极端层级容错有严苛要求的场景中,基于递归或迭代栈的手写实现则是保障系统稳健运行的坚实底座。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01







![JS创建二维数组:为什么Array(n).fill([])会导致数据联动](https://img.javascript.cn/tit/articles/13236/20261102/f618c991891ff37efb12df6af2ce0ccb0da8779a907fe1d2f7275d60c84fdd77.webp)
