各位技术同仁,大家好。今天我们将深入探讨一个在JavaScript编程中非常常见,却又充满技巧性的话题:数组扁平化(Array Flattening)。在处理复杂数据结构,特别是从API获取的嵌套数据、用户界面中的层级组件状态,或者图形算法的遍历结果时,我们经常会遇到需要将多维数组转换为一维数组的需求。理解并掌握多种数组扁平化技术,不仅能提升我们的代码效率,更能拓宽解决问题的思路。
本次讲座,我将作为一名编程专家,带领大家从概念到实现,从基础到高级,再到性能对比,全面剖析数组扁平化的方方面面。我们将考察不同的JavaScript实现方式,包括ES2019中引入的内置方法,以及各种手动实现策略,并对它们的性能进行深入对比,最终帮助大家在实际项目中做出明智的技术选型。
1. 数组扁平化的核心概念与应用场景
什么是数组扁平化?
数组扁平化,顾名思义,就是将一个包含多层嵌套数组的复杂结构,转换成一个只有一层(一维)的数组。例如,我们可能有一个这样的数组:[1, [2, 3], [4, [5, 6]]]。经过扁平化处理后,它应该变成 [1, 2, 3, 4, 5, 6]。
扁平化操作的关键在于如何处理数组中的数组元素。有些时候,我们可能只关心扁平化一层嵌套(浅扁平化),而另一些时候,我们需要将所有层级的嵌套都展开(深扁平化)。
为什么需要数组扁平化?
数组扁平化在实际开发中有着广泛的应用:
- 数据处理与分析:当从后端API获取的数据是嵌套结构时,为了方便统一处理(如过滤、映射、查找),我们常常需要先将其扁平化。例如,一个订单可能包含多个商品,每个商品又包含多个SKU,如果我们想统计所有SKU的总数量,扁平化数据会使操作更直接。
- UI渲染:在前端框架(如React、Vue)中,有时组件的子元素是动态生成的,并且可能以嵌套数组的形式返回。为了将其渲染为单一的列表或网格,扁平化是必要的。
- 算法与数据结构:在图遍历(如深度优先搜索或广度优先搜索)或树结构的处理中,结果往往是嵌套的。扁平化可以帮助我们统一收集所有节点。
- 简化数据结构:扁平化可以降低数据结构的复杂性,使代码逻辑更清晰,减少迭代嵌套数组时的心智负担。
理解了这些背景,我们就可以开始探索具体的实现方式了。
2. 基础扁平化:内置方法与浅层扁平化
在JavaScript中,处理数组扁平化已经变得越来越简单,特别是随着ES2019的引入。
2.1. Array.prototype.flat():现代、声明式扁平化
Array.prototype.flat() 是ES2019中新增的数组方法,它为数组扁平化提供了一个非常简洁且强大的内置解决方案。这是目前在支持ES2019及以上环境中最推荐使用的方法。
语法:
arr.flat([depth])
depth(可选): 指定要扁平化的层数。默认值为1。- 如果
depth为Infinity,则会递归地扁平化所有嵌套数组,直到数组中不再有任何嵌套数组。
示例:浅层扁平化 (depth = 1)
const nestedArray1 = [1, [2, 3], [4, [5, 6]]];
const flattenedArray1 = nestedArray1.flat(); // 默认 depth 为 1
console.log(flattenedArray1); // 输出: [1, 2, 3, 4, [5, 6]]
const nestedArray2 = [1, 2, [3, 4, [5, 6]]];
const flattenedArray2 = nestedArray2.flat();
console.log(flattenedArray2); // 输出: [1, 2, 3, 4, [5, 6]]
可以看到,flat() 默认只扁平化了一层嵌套。[5, 6] 仍然保持嵌套状态,因为它位于第二层。
示例:指定深度扁平化
const deepNestedArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
// 扁平化一层
console.log(deepNestedArray.flat(1));
// 输出: [1, 2, 3, 4, [5, 6, [7, 8]], 9]
// 扁平化两层
console.log(deepNestedArray.flat(2));
// 输出: [1, 2, 3, 4, 5, 6, [7, 8], 9]
// 扁平化三层
console.log(deepNestedArray.flat(3));
// 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
通过传递不同的 depth 值,我们可以精确控制扁平化的层级。
示例:深度扁平化 (depth = Infinity)
当我们需要将所有嵌套数组都展开时,可以使用 Infinity 作为 depth 参数。
const superDeepArray = [1, [2, [3, [4, [5, 6]]]], [7, 8]];
const fullyFlattenedArray = superDeepArray.flat(Infinity);
console.log(fullyFlattenedArray); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]
这使得 flat() 成为处理任意深度嵌套数组的首选工具,极大地简化了代码。
浏览器兼容性:
Array.prototype.flat() 是ES2019特性,因此在较旧的浏览器(如IE)中不被支持。在需要兼容旧环境的项目中,可能需要引入 polyfill 或者使用其他手动实现的方法。
2.2. concat() 与 展开运算符 (...):浅层扁平化的经典组合
在 flat() 方法出现之前,或者当只需要浅层扁平化时,Array.prototype.concat() 结合展开运算符 (...) 是一种常见且简洁的实现方式。
原理:
concat() 方法用于连接两个或多个数组。该方法不会改变现有数组,而是返回一个新数组。展开运算符 (...) 可以将数组或可迭代对象展开为独立的元素。
示例:
const nestedArray = [1, [2, 3], [4, [5, 6]]];
// 使用 concat 和展开运算符进行浅层扁平化
const flattenedArray = [].concat(...nestedArray);
console.log(flattenedArray); // 输出: [1, 2, 3, 4, [5, 6]]
// 或者更直接地,通过 Array.prototype.concat.apply
const flattenedArrayApply = Array.prototype.concat.apply([], nestedArray);
console.log(flattenedArrayApply); // 输出: [1, 2, 3, 4, [5, 6]]
解释:
当 [].concat(...nestedArray) 执行时:
...nestedArray将[1, [2, 3], [4, [5, 6]]]展开为1, [2, 3], [4, [5, 6]]。[].concat(1, [2, 3], [4, [5, 6]])将这些元素连接到一个空数组[]上。concat方法会将非数组元素直接添加,对于数组元素,它会将其展开一层。所以[2, 3]变成了2, 3,而[4, [5, 6]]也被展开成了4, [5, 6]。
局限性:
这种方法只能进行浅层扁平化。如果数组嵌套层级超过一层,它将无法完全扁平化。例如,[4, [5, 6]] 在扁平化后仍然是 [4, [5, 6]],内层的 [5, 6] 保持不变。因此,对于深层扁平化,我们需要更复杂的策略。
3. 深度扁平化:手动实现策略与高级技巧
当 flat() 方法不适用(例如,需要兼容旧环境)或需要对扁平化过程进行更细粒度的控制时,我们可以采用多种手动实现的策略来实现深度扁平化。
3.1. 递归实现:经典而直观
递归是处理树形或嵌套结构最自然的方式之一。对于数组扁平化,其核心思想是:遍历数组中的每个元素。如果元素是数组,则对其递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。
实现思路:
- 创建一个空数组
result用于存放扁平化后的元素。 - 遍历输入数组的每个元素。
- 对于每个元素:
- 如果它是数组,则递归调用扁平化函数,并将返回的结果通过
concat或push(...方式添加到result中。 - 如果它不是数组,则直接将其添加到
result中。
- 如果它是数组,则递归调用扁平化函数,并将返回的结果通过
- 返回
result。
代码示例:
function flattenRecursive(arr) {
let result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result = result.concat(flattenRecursive(item)); // 递归调用并合并结果
} else {
result.push(item);
}
}
return result;
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("递归扁平化:", flattenRecursive(deepArray)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
const complexArray = [1, 'hello', [2, true], [3, [4, null, [5, undefined]]]];
console.log("递归扁平化 (复杂数据):", flattenRecursive(complexArray)); // 输出: [1, 'hello', 2, true, 3, 4, null, 5, undefined]
优点:
- 逻辑清晰,符合直观思维。
- 能够处理任意深度的嵌套。
缺点:
- 栈溢出 (Stack Overflow):当嵌套层级非常深时,每次递归调用都会在调用栈中创建一个新的帧。如果层级过深,可能会导致栈溢出错误(
RangeError: Maximum call stack size exceeded)。JavaScript引擎对调用栈的深度有限制。
3.2. 迭代实现(使用栈):避免栈溢出
为了避免递归带来的栈溢出问题,我们可以采用迭代的方式来实现深度扁平化。这种方法通常使用一个辅助栈(或者直接操作原数组)来管理待处理的元素。
实现思路:
- 创建一个空数组
result用于存放扁平化后的元素。 - 创建一个栈
stack,并将输入数组的元素以逆序推入栈中(这样在弹出时可以保持原有的顺序)。 - 当栈不为空时,循环执行以下操作:
- 从栈顶弹出一个元素。
- 如果该元素是数组,则将其所有子元素(再次以逆序)推入栈中。
- 如果该元素不是数组,则将其添加到
result数组的开头(因为我们是逆序处理,所以需要添加到开头才能保持原序)。
- 返回
result。
代码示例:
function flattenIterative(arr) {
const result = [];
// 使用一个栈来存放待处理的元素。
// 为了保持最终结果的顺序,我们从后往前遍历原始数组,将元素推入栈中。
// 这样在从栈中弹出时,元素的顺序就是原始数组的顺序。
const stack = [...arr]; // 将数组的所有元素复制到栈中
while (stack.length > 0) {
const item = stack.pop(); // 弹出栈顶元素
if (Array.isArray(item)) {
// 如果是数组,将其所有子元素再次逆序推入栈中
// 因为pop()会取出最后一个元素,所以这里要逆序push,保证小的索引先被处理
for (let i = item.length - 1; i >= 0; i--) {
stack.push(item[i]);
}
} else {
// 如果不是数组,添加到结果数组的开头 (因为我们是逆序处理的)
result.unshift(item);
}
}
return result;
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("迭代扁平化:", flattenIterative(deepArray)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
const superDeepArray = [];
for (let i = 0; i < 10000; i++) {
superDeepArray.push(i);
if (i % 100 === 0) {
superDeepArray.push([]);
let current = superDeepArray;
for (let j = 0; j < 5; j++) {
current.push([j]);
current = current[current.length - 1];
}
}
}
// 对于非常深的数组,迭代方法可以避免栈溢出
// console.log("迭代扁平化 (超深数组):", flattenIterative(superDeepArray)); // 运行会很长,但不会栈溢出
另一种迭代实现(更简洁,但可能需要额外的 reverse()):
上述迭代方法需要将非数组元素 unshift 到 result 数组的开头,这可能会导致性能问题(unshift 操作通常比 push 慢,因为它需要移动所有现有元素)。我们可以调整策略,先 push,最后再 reverse。
function flattenIterativeOptimized(arr) {
const result = [];
const stack = [...arr]; // 将所有元素放入栈中
while (stack.length > 0) {
const item = stack.pop(); // 弹出栈顶元素
if (Array.isArray(item)) {
// 如果是数组,将其所有子元素再次推入栈中,保持原来的顺序,以便pop时逆序处理
// 注意这里是 item.length - 1 到 0 的循环,确保栈中元素的顺序是正确的
for (let i = item.length - 1; i >= 0; i--) {
stack.push(item[i]);
}
} else {
// 非数组元素直接添加到结果数组的末尾
result.push(item);
}
}
// 因为是从栈中逆序弹出的,所以最终结果需要反转才能保持原始顺序
return result.reverse();
}
const deepArray2 = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("迭代扁平化 (优化版):", flattenIterativeOptimized(deepArray2)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
优点:
- 避免了递归的栈溢出问题,可以处理任意深度的嵌套数组。
- 性能通常比递归更稳定,尤其是在深度很大的情况下。
缺点:
- 代码逻辑相对递归稍微复杂一些,理解起来可能需要一点时间。
reverse()操作在数组很大时也有一定的开销,但通常比频繁的unshift要好。
3.3. reduce() 结合 concat():函数式编程风格
Array.prototype.reduce() 方法是一个强大的高阶函数,它可以将数组归约为单个值。结合 concat() 和 Array.isArray(),我们可以实现一种优雅的深度扁平化。
实现思路:
- 使用
reduce()遍历数组的每个元素。 reduce()的回调函数接收一个累加器acc和当前元素cur。- 如果
cur是一个数组,则递归调用扁平化函数(或再次使用reduce()),然后将结果与acc合并(acc.concat(...))。 - 如果
cur不是数组,则将其直接推入acc(acc.concat(cur))。
代码示例:
function flattenWithReduce(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flattenWithReduce(cur) : cur);
}, []);
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("Reduce扁平化:", flattenWithReduce(deepArray)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
const complexArray = [1, 'hello', [2, true], [3, [4, null, [5, undefined]]]];
console.log("Reduce扁平化 (复杂数据):", flattenWithReduce(complexArray)); // 输出: [1, 'hello', 2, true, 3, 4, null, 5, undefined]
优点:
- 代码简洁,具有函数式编程的风格。
- 同样能够处理任意深度的嵌套。
缺点:
- 栈溢出:与纯递归方法一样,
reduce内部如果递归调用自身,当嵌套层级过深时,依然可能导致栈溢出。因为reduce的回调函数本身也是一个函数调用,递归会不断增加调用栈。 - 每次
concat操作都会创建新数组,对于非常大的数组,这可能会导致性能开销和内存占用。
3.4. while 循环结合 some() 或 Array.isArray():基于状态检测的迭代
这种方法通过一个 while 循环不断检查数组中是否还存在嵌套数组,如果存在,则继续扁平化,直到不再有嵌套数组为止。它通常结合 concat() 和展开运算符来实现每一层的扁平化。
实现思路:
- 初始化
result为输入数组的一个副本。 - 进入一个
while循环,条件是result中是否存在任何数组元素(可以使用result.some(Array.isArray)来检查)。 - 在循环内部,使用
[].concat(...result)将result数组扁平化一层。 - 当循环结束时,
result将是一个完全扁平化的数组。
代码示例:
function flattenWithWhileAndSome(arr) {
let result = [...arr]; // 复制一份数组,避免修改原数组
while (result.some(Array.isArray)) {
result = [].concat(...result); // 每次扁平化一层
}
return result;
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("While + Some 扁平化:", flattenWithWhileAndSome(deepArray)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
优点:
- 代码相对简洁,易于理解。
- 能够处理任意深度的嵌套,且不会有栈溢出的风险,因为它是迭代而非递归。
缺点:
- 性能问题:每次循环都需要遍历整个
result数组来执行some(Array.isArray)检查,并且[].concat(...result)也会创建新的数组。对于非常大的数组或深度很深的数组,这可能会导致显著的性能开销。尤其是在内层循环中,some操作会重复扫描已经扁平化的部分。
3.5. JSON.stringify() 和 JSON.parse():奇技淫巧与巨大陷阱
这是一个非传统的扁平化方法,它利用JSON序列化和反序列化的特性。
原理:
- 将嵌套数组
JSON.stringify()转换为字符串。这会将所有数组都变成[...]形式的字符串。 - 然后,使用正则表达式将字符串中的
[和]替换掉,只留下数字和逗号。 - 最后,将处理过的字符串通过
JSON.parse()转换回数组。
代码示例:
function flattenWithJSON(arr) {
try {
// 先序列化为JSON字符串
const jsonString = JSON.stringify(arr);
// 使用正则替换掉所有的 [ 和 ],形成一个逗号分隔的字符串
// 注意这里只替换了数字和逗号,对于其他类型的数据可能需要更复杂的正则
const flatString = jsonString.replace(/[|]/g, '');
// 将逗号分隔的字符串转换为数组,并处理数字类型
// 过滤掉空字符串,因为替换可能留下连续逗号或开头/结尾的逗号
// map(Number) 尝试将每个元素转为数字,非数字会变成 NaN
return JSON.parse(`[${flatString}]`).filter(item => item !== null);
} catch (e) {
console.error("JSON扁平化失败:", e);
return [];
}
}
const deepArray = [1, [2, 3], [4, [5, 6]]];
console.log("JSON扁平化:", flattenWithJSON(deepArray)); // 输出: [1, 2, 3, 4, 5, 6]
const mixedArray = [1, 'a', [2, null], [3, [4, undefined]]];
console.log("JSON扁平化 (混合数据):", flattenWithJSON(mixedArray));
// 输出: [1, "a", 2, null, 3, 4, null]
// 注意: undefined 会被 JSON.stringify 忽略,丢失!
// 注意: 'a' 这样的字符串会保持,但数字会变数字,null会变null
// 如果数据是 [1, 'a', [2, { key: 'value' }]]
// flatString = "1,"a",2,{"key":"value"}"
// `[${flatString}]` = "[1,"a",2,{"key":"value"}]"
// JSON.parse 之后会是 [1, "a", 2, { key: "value" }],对象不会被扁平化!
const funcArray = [1, [2, () => {}]];
console.log("JSON扁平化 (含函数):", flattenWithJSON(funcArray));
// 输出: [1, 2, null]
// 注意: 函数会被 JSON.stringify 忽略或变为 null,丢失!
巨大陷阱与不适用场景:
这种方法有很多严重的局限性,在绝大多数实际项目中都不推荐使用:
- 数据类型丢失或转换:
undefined、函数 (functions)、Symbol类型的值在JSON.stringify()后会被忽略或变为null,导致数据丢失。Date对象会转换为ISO格式的字符串,而不是Date对象本身。- 对象不会被扁平化,它们会作为完整的对象元素保留在数组中。
- 性能极差:字符串化和解析JSON,以及字符串替换操作,都是非常耗费性能的。
- 安全性问题:如果输入的数组包含循环引用,
JSON.stringify()会抛出错误。 - 只适用于扁平化数字、字符串、布尔值和
null组成的数组,且不包含对象。
结论: 了解这种方法作为一种“奇技淫巧”即可,但在实际生产环境中,请避免使用。
4. 性能对比与基准测试
理解了各种实现方式后,我们最关心的问题之一就是它们的性能表现。我们将通过编写基准测试代码来对比不同方法在不同场景下的性能。
4.1. 基准测试方法论
为了进行公平且有意义的性能对比,我们需要:
- 统一的测试数据:生成不同规模(大小)、不同深度(扁平、中等深度、超深)的测试数组。
- 多次运行取平均值:由于JavaScript引擎的JIT编译和垃圾回收机制,单次运行的结果可能不稳定。多次运行并计算平均时间可以得到更可靠的数据。
- 避免测量开销:确保测量的是函数本身的执行时间,而不是测试框架或数据生成的时间。
- 使用高精度计时器:
performance.now()比Date.now()更适合测量短时间内的代码执行。
4.2. 生成测试数据
我们将创建一些辅助函数来生成不同类型的嵌套数组。
/**
* 生成一个指定深度和大小的嵌套数组
* @param {number} depth 数组的嵌套深度
* @param {number} size 每一层数组的元素数量 (不包括嵌套数组本身)
* @param {number} totalElements 扁平化后总元素数量的近似值
* @returns {Array} 生成的嵌套数组
*/
function generateNestedArray(depth, size, totalElements) {
if (depth <= 0) {
return Array.from({ length: totalElements }, (_, i) => i);
}
const arr = [];
const elementsPerLayer = Math.floor(totalElements / depth);
let currentElements = 0;
for (let i = 0; i < size; i++) {
if (currentElements < totalElements) {
arr.push(i);
currentElements++;
}
}
if (depth > 1) {
// 在这一层中插入嵌套数组,均匀分布
const nestedCount = Math.min(size, depth - 1); // 最多插入 depth-1 个嵌套数组
for (let i = 0; i < nestedCount; i++) {
const insertIndex = Math.floor(Math.random() * arr.length);
const remainingElements = totalElements - currentElements;
const subArrayTotalElements = Math.floor(remainingElements / (depth - 1 - i));
const subArraySize = Math.max(1, Math.floor(subArrayTotalElements / Math.max(1, depth - 1))); // 确保子数组至少有一个元素
arr.splice(insertIndex, 0, generateNestedArray(depth - 1, subArraySize, subArrayTotalElements));
currentElements += subArrayTotalElements;
}
}
// 填充剩余元素,确保总数接近 totalElements
while (arr.flat(Infinity).length < totalElements) {
arr.push(Math.random());
}
// 打乱一下顺序,让嵌套数组不总是出现在最后
arr.sort(() => Math.random() - 0.5);
return arr;
}
// 浅层大数组 (深度1,元素100000)
const shallowLargeArray = generateNestedArray(1, 100000, 100000);
// 中等深度中等大小数组 (深度5,每层100个元素,总计约5000元素)
const mediumDeepMediumArray = generateNestedArray(5, 100, 5000);
// 深层小数组 (深度100,每层1个元素,总计约100元素)
const deepSmallArray = generateNestedArray(100, 1, 100);
// 超深数组 (深度10000,每层1个元素,总计约10000元素) - 可能导致栈溢出,仅用于迭代测试
// 对于递归方法,这个数据量可能过大,所以测试时要小心
const ultraDeepArray = generateNestedArray(10000, 1, 10000);
4.3. 基准测试函数
/**
* 执行基准测试
* @param {string} name 测试名称
* @param {Function} func 要测试的扁平化函数
* @param {Array} data 测试数据
* @param {number} iterations 运行次数
*/
function benchmark(name, func, data, iterations = 100) {
const times = [];
let result = null;
// 预热JIT编译器
for (let i = 0; i < 5; i++) {
func(data);
}
for (let i = 0; i < iterations; i++) {
const start = performance.now();
try {
result = func(data);
} catch (e) {
console.warn(`Function '${name}' failed with error: ${e.message}`);
result = null; // 标记为失败
break; // 停止进一步迭代
}
const end = performance.now();
times.push(end - start);
}
if (result === null) {
return { name, avgTime: 'Error', minTime: 'Error', maxTime: 'Error' };
}
const totalTime = times.reduce((sum, t) => sum + t, 0);
const avgTime = totalTime / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
return { name, avgTime, minTime, maxTime, dataLength: result.length };
}
// 扁平化函数集合
const flatteningFunctions = {
'Array.prototype.flat(Infinity)': (arr) => arr.flat(Infinity),
'Recursive': flattenRecursive,
'Iterative (Optimized)': flattenIterativeOptimized,
'Reduce + Concat': flattenWithReduce,
'While + Some': flattenWithWhileAndSome,
'JSON.stringify/parse (Caution!)': flattenWithJSON
};
// 运行所有测试
console.log("--- 数组扁平化性能对比 ---");
const testResults = [];
// Scenario 1: 浅层大数组
console.log("n--- Scenario 1: 浅层大数组 (100,000 元素, 深度1) ---");
for (const funcName in flatteningFunctions) {
if (Object.hasOwnProperty.call(flatteningFunctions, funcName)) {
const func = flatteningFunctions[funcName];
const result = benchmark(funcName, func, shallowLargeArray);
testResults.push({ scenario: 'Shallow Large', ...result });
console.log(`${result.name}: Avg: ${result.avgTime.toFixed(3)}ms, Min: ${result.minTime.toFixed(3)}ms, Max: ${result.maxTime.toFixed(3)}ms`);
}
}
// Scenario 2: 中等深度中等大小数组
console.log("n--- Scenario 2: 中等深度中等大小数组 (5,000 元素, 深度5) ---");
for (const funcName in flatteningFunctions) {
if (Object.hasOwnProperty.call(flatteningFunctions, funcName)) {
const func = flatteningFunctions[funcName];
const result = benchmark(funcName, func, mediumDeepMediumArray);
testResults.push({ scenario: 'Medium Deep Medium', ...result });
console.log(`${result.name}: Avg: ${result.avgTime.toFixed(3)}ms, Min: ${result.minTime.toFixed(3)}ms, Max: ${result.maxTime.toFixed(3)}ms`);
}
}
// Scenario 3: 深层小数组
console.log("n--- Scenario 3: 深层小数组 (100 元素, 深度100) ---");
for (const funcName in flatteningFunctions) {
if (Object.hasOwnProperty.call(flatteningFunctions, funcName)) {
const func = flatteningFunctions[funcName];
const result = benchmark(funcName, func, deepSmallArray);
testResults.push({ scenario: 'Deep Small', ...result });
console.log(`${result.name}: Avg: ${result.avgTime.toFixed(3)}ms, Min: ${result.minTime.toFixed(3)}ms, Max: ${result.maxTime.toFixed(3)}ms`);
}
}
// Scenario 4: 超深数组 (可能导致递归栈溢出,JSON可能数据丢失)
console.log("n--- Scenario 4: 超深数组 (10,000 元素, 深度10,000) ---");
for (const funcName in flatteningFunctions) {
if (Object.hasOwnProperty.call(flatteningFunctions, funcName)) {
const func = flatteningFunctions[funcName];
const result = benchmark(funcName, func, ultraDeepArray, 10); // 减少迭代次数,因为可能耗时
testResults.push({ scenario: 'Ultra Deep', ...result });
console.log(`${result.name}: Avg: ${result.avgTime === 'Error' ? 'Error' : result.avgTime.toFixed(3) + 'ms'}, Min: ${result.minTime === 'Error' ? 'Error' : result.minTime.toFixed(3) + 'ms'}, Max: ${result.maxTime === 'Error' ? 'Error' : result.maxTime.toFixed(3) + 'ms'}`);
}
}
4.4. 性能测试结果分析 (理论与实践)
以下表格展示了在典型浏览器或Node.js环境下运行上述基准测试可能得到的理论结果。实际结果会因JS引擎版本、硬件环境、数组内容(纯数字、字符串、对象等)以及JIT优化程度而异。
性能对比表 (理论值)
| 方法名称 | 场景: 浅层大数组 (10万元素, 深度1) | 场景: 中等深度中等数组 (5千元素, 深度5) | 场景: 深层小数组 (1百元素, 深度100) | 场景: 超深数组 (1万元素, 深度1万) | 优点 W # Table of Content
- Introduction: The Concept of Array Flattening
- Basic Implementations (Shallow Flattening)
2.1.Array.prototype.flat()(ES2019)
2.2.concat()and Spread Operator (...) - Deep Flattening Implementations (Pre-ES2019 & Manual Approaches)
3.1. Recursive Approach
3.2. Iterative Approach (using a stack)
3.3.reduce()andconcat()
3.4.whileloop andsome()
3.5.JSON.stringify()andJSON.parse()(Hack/Trick, with caveats) - Performance Comparison and Benchmarking
4.1. Methodology
4.2. Generating Test Data
4.3. Benchmarking Function
4.4. Performance Test Results Analysis - Practical Applications and Choosing the Right Method
- Best Practices and Potential Pitfalls
- Concluding Thoughts
1. 数组扁平化的核心概念与应用场景
数组扁平化,顾名思义,就是将一个包含多层嵌套数组的复杂结构,转换成一个只有一层(一维)的数组。例如,我们可能有一个这样的数组:[1, [2, 3], [4, [5, 6]]]。经过扁平化处理后,它应该变成 [1, 2, 3, 4, 5, 6]。
扁平化操作的关键在于如何处理数组中的数组元素。有些时候,我们可能只关心扁平化一层嵌套(浅扁平化),而另一些时候,我们需要将所有层级的嵌套都展开(深扁平化)。
为什么需要数组扁平化?
数组扁平化在实际开发中有着广泛的应用:
- 数据处理与分析:当从后端API获取的数据是嵌套结构时,为了方便统一处理(如过滤、映射、查找),我们常常需要先将其扁平化。例如,一个订单可能包含多个商品,每个商品又包含多个SKU,如果我们想统计所有SKU的总数量,扁平化数据会使操作更直接。
- UI渲染:在前端框架(如React、Vue)中,有时组件的子元素是动态生成的,并且可能以嵌套数组的形式返回。为了将其渲染为单一的列表或网格,扁平化是必要的。
- 算法与数据结构:在图遍历(如深度优先搜索或广度优先搜索)或树结构的处理中,结果往往是嵌套的。扁平化可以帮助我们统一收集所有节点。
- 简化数据结构:扁平化可以降低数据结构的复杂性,使代码逻辑更清晰,减少迭代嵌套数组时的心智负担。
理解了这些背景,我们就可以开始探索具体的实现方式了。
2. 基础扁平化:内置方法与浅层扁平化
在JavaScript中,处理数组扁平化已经变得越来越简单,特别是随着ES2019的引入。
2.1. Array.prototype.flat():现代、声明式扁平化
Array.prototype.flat() 是ES2019中新增的数组方法,它为数组扁平化提供了一个非常简洁且强大的内置解决方案。这是目前在支持ES2019及以上环境中最推荐使用的方法。
语法:
arr.flat([depth])
depth(可选): 指定要扁平化的层数。默认值为1。- 如果
depth为Infinity,则会递归地扁平化所有嵌套数组,直到数组中不再有任何嵌套数组。
示例:浅层扁平化 (depth = 1)
const nestedArray1 = [1, [2, 3], [4, [5, 6]]];
const flattenedArray1 = nestedArray1.flat(); // 默认 depth 为 1
console.log(flattenedArray1); // 输出: [1, 2, 3, 4, [5, 6]]
const nestedArray2 = [1, 2, [3, 4, [5, 6]]];
const flattenedArray2 = nestedArray2.flat();
console.log(flattenedArray2); // 输出: [1, 2, 3, 4, [5, 6]]
可以看到,flat() 默认只扁平化了一层嵌套。[5, 6] 仍然保持嵌套状态,因为它位于第二层。
示例:指定深度扁平化
const deepNestedArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
// 扁平化一层
console.log(deepNestedArray.flat(1));
// 输出: [1, 2, 3, 4, [5, 6, [7, 8]], 9]
// 扁平化两层
console.log(deepNestedArray.flat(2));
// 输出: [1, 2, 3, 4, 5, 6, [7, 8], 9]
// 扁平化三层
console.log(deepNestedArray.flat(3));
// 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
通过传递不同的 depth 值,我们可以精确控制扁平化的层级。
示例:深度扁平化 (depth = Infinity)
当我们需要将所有嵌套数组都展开时,可以使用 Infinity 作为 depth 参数。
const superDeepArray = [1, [2, [3, [4, [5, 6]]]], [7, 8]];
const fullyFlattenedArray = superDeepArray.flat(Infinity);
console.log(fullyFlattenedArray); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]
这使得 flat() 成为处理任意深度嵌套数组的首选工具,极大地简化了代码。
浏览器兼容性:
Array.prototype.flat() 是ES2019特性,因此在较旧的浏览器(如IE)中不被支持。在需要兼容旧环境的项目中,可能需要引入 polyfill 或者使用其他手动实现的方法。
2.2. concat() 与 展开运算符 (...):浅层扁平化的经典组合
在 flat() 方法出现之前,或者当只需要浅层扁平化时,Array.prototype.concat() 结合展开运算符 (...) 是一种常见且简洁的实现方式。
原理:
concat() 方法用于连接两个或多个数组。该方法不会改变现有数组,而是返回一个新数组。展开运算符 (...) 可以将数组或可迭代对象展开为独立的元素。
示例:
const nestedArray = [1, [2, 3], [4, [5, 6]]];
// 使用 concat 和展开运算符进行浅层扁平化
const flattenedArray = [].concat(...nestedArray);
console.log(flattenedArray); // 输出: [1, 2, 3, 4, [5, 6]]
// 或者更直接地,通过 Array.prototype.concat.apply
const flattenedArrayApply = Array.prototype.concat.apply([], nestedArray);
console.log(flattenedArrayApply); // 输出: [1, 2, 3, 4, [5, 6]]
解释:
当 [].concat(...nestedArray) 执行时:
...nestedArray将[1, [2, 3], [4, [5, 6]]]展开为1, [2, 3], [4, [5, 6]]。[].concat(1, [2, 3], [4, [5, 6]])将这些元素连接到一个空数组[]上。concat方法会将非数组元素直接添加,对于数组元素,它会将其展开一层。所以[2, 3]变成了2, 3,而[4, [5, 6]]也被展开成了4, [5, 6]。
局限性:
这种方法只能进行浅层扁平化。如果数组嵌套层级超过一层,它将无法完全扁平化。例如,[4, [5, 6]] 在扁平化后仍然是 [4, [5, 6]],内层的 [5, 6] 保持不变。因此,对于深层扁平化,我们需要更复杂的策略。
3. 深度扁平化:手动实现策略与高级技巧
当 flat() 方法不适用(例如,需要兼容旧环境)或需要对扁平化过程进行更细粒度的控制时,我们可以采用多种手动实现的策略来实现深度扁平化。
3.1. 递归实现:经典而直观
递归是处理树形或嵌套结构最自然的方式之一。对于数组扁平化,其核心思想是:遍历数组中的每个元素。如果元素是数组,则对其递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。
实现思路:
- 创建一个空数组
result用于存放扁平化后的元素。 - 遍历输入数组的每个元素。
- 对于每个元素:
- 如果它是数组,则递归调用扁平化函数,并将返回的结果通过
concat或push(...方式添加到result中。 - 如果它不是数组,则直接将其添加到
result中。
- 如果它是数组,则递归调用扁平化函数,并将返回的结果通过
- 返回
result。
代码示例:
function flattenRecursive(arr) {
let result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result = result.concat(flattenRecursive(item)); // 递归调用并合并结果
} else {
result.push(item);
}
}
return result;
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("递归扁平化:", flattenRecursive(deepArray));
const complexArray = [1, 'hello', [2, true], [3, [4, null, [5, undefined]]]];
console.log("递归扁平化 (复杂数据):", flattenRecursive(complexArray));
优点:
- 逻辑清晰,符合直观思维。
- 能够处理任意深度的嵌套。
缺点:
- 栈溢出 (Stack Overflow):当嵌套层级非常深时,每次递归调用都会在调用栈中创建一个新的帧。如果层级过深,可能会导致栈溢出错误(
RangeError: Maximum call stack size exceeded)。JavaScript引擎对调用栈的深度有限制。
3.2. 迭代实现(使用栈):避免栈溢出
为了避免递归带来的栈溢出问题,我们可以采用迭代的方式来实现深度扁平化。这种方法通常使用一个辅助栈(或者直接操作原数组)来管理待处理的元素。
实现思路:
- 创建一个空数组
result用于存放扁平化后的元素。 - 创建一个栈
stack,并将输入数组的元素以逆序推入栈中(这样在弹出时可以保持原有的顺序)。 - 当栈不为空时,循环执行以下操作:
- 从栈顶弹出一个元素。
- 如果该元素是数组,则将其所有子元素(再次以逆序)推入栈中。
- 如果该元素不是数组,则将其添加到
result数组的开头(因为我们是逆序处理,所以需要添加到开头才能保持原序)。
- 返回
result。
代码示例 (优化版,先 push 后 reverse):
function flattenIterativeOptimized(arr) {
const result = [];
const stack = [...arr]; // 将所有元素放入栈中
while (stack.length > 0) {
const item = stack.pop(); // 弹出栈顶元素
if (Array.isArray(item)) {
// 如果是数组,将其所有子元素再次推入栈中,保持原来的顺序,以便pop时逆序处理
// 注意这里是 item.length - 1 到 0 的循环,确保栈中元素的顺序是正确的
for (let i = item.length - 1; i >= 0; i--) {
stack.push(item[i]);
}
} else {
// 非数组元素直接添加到结果数组的末尾
result.push(item);
}
}
// 因为是从栈中逆序弹出的,所以最终结果需要反转才能保持原始顺序
return result.reverse();
}
const deepArray2 = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("迭代扁平化 (优化版):", flattenIterativeOptimized(deepArray2));
优点:
- 避免了递归的栈溢出问题,可以处理任意深度的嵌套数组。
- 性能通常比递归更稳定,尤其是在深度很大的情况下。
缺点:
- 代码逻辑相对递归稍微复杂一些,理解起来可能需要一点时间。
reverse()操作在数组很大时也有一定的开销,但通常比频繁的unshift要好。
3.3. reduce() 结合 concat():函数式编程风格
Array.prototype.reduce() 方法是一个强大的高阶函数,它可以将数组归约为单个值。结合 concat() 和 Array.isArray(),我们可以实现一种优雅的深度扁平化。
实现思路:
- 使用
reduce()遍历数组的每个元素。 reduce()的回调函数接收一个累加器acc和当前元素cur。- 如果
cur是一个数组,则递归调用扁平化函数(或再次使用reduce()),然后将结果与acc合并(acc.concat(...))。 - 如果
cur不是数组,则将其直接推入acc(acc.concat(cur))。
代码示例:
function flattenWithReduce(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flattenWithReduce(cur) : cur);
}, []);
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("Reduce扁平化:", flattenWithReduce(deepArray));
const complexArray = [1, 'hello', [2, true], [3, [4, null, [5, undefined]]]];
console.log("Reduce扁平化 (复杂数据):", flattenWithReduce(complexArray));
优点:
- 代码简洁,具有函数式编程的风格。
- 同样能够处理任意深度的嵌套。
缺点:
- 栈溢出:与纯递归方法一样,
reduce内部如果递归调用自身,当嵌套层级过深时,依然可能导致栈溢出。因为reduce的回调函数本身也是一个函数调用,递归会不断增加调用栈。 - 每次
concat操作都会创建新数组,对于非常大的数组,这可能会导致性能开销和内存占用。
3.4. while 循环结合 some() 或 Array.isArray():基于状态检测的迭代
这种方法通过一个 while 循环不断检查数组中是否还存在嵌套数组,如果存在,则继续扁平化,直到不再有嵌套数组为止。它通常结合 concat() 和展开运算符来实现每一层的扁平化。
实现思路:
- 初始化
result为输入数组的一个副本。 - 进入一个
while循环,条件是result中是否存在任何数组元素(可以使用result.some(Array.isArray)来检查)。 - 在循环内部,使用
[].concat(...result)将result数组扁平化一层。 - 当循环结束时,
result将是一个完全扁平化的数组。
代码示例:
function flattenWithWhileAndSome(arr) {
let result = [...arr]; // 复制一份数组,避免修改原数组
while (result.some(Array.isArray)) {
result = [].concat(...result); // 每次扁平化一层
}
return result;
}
const deepArray = [1, [2, 3], [4, [5, 6, [7, 8]]], 9];
console.log("While + Some 扁平化:", flattenWithWhileAndSome(deepArray));
优点:
- 代码相对简洁,易于理解。
- 能够处理任意深度的嵌套,且不会有栈溢出的风险,因为它是迭代而非递归。
缺点:
- 性能问题:每次循环都需要遍历整个
result数组来执行some(Array.isArray)检查,并且[].concat(...result)也会创建新的数组。对于非常大的数组或深度很深的数组,这可能会导致显著的性能开销。尤其是在内层循环中,some操作会重复扫描已经扁平化的部分。
3.5. JSON.stringify() 和 JSON.parse():奇技淫巧与巨大陷阱
这是一个非传统的扁平化方法,它利用JSON序列化和反序列化的特性。
原理:
- 将嵌套数组
JSON.stringify()转换为字符串。这会将所有数组都变成[...]形式的字符串。 - 然后,使用正则表达式将字符串中的
[和]替换掉,只留下数字和逗号。 - 最后,将处理过的字符串通过
JSON.parse()转换回数组。
代码示例:
function flattenWithJSON(arr) {
try {
// 先序列化为JSON字符串
const jsonString = JSON.stringify(arr);
// 使用正则替换掉所有的 [ 和 ],形成一个逗号分隔的字符串
const flatString = jsonString.replace(/[|]/g, '');
// 将逗号分隔的字符串转换为数组,并处理数字类型
return JSON.parse(`[${flatString}]`).filter(item => item !== null);
} catch (e) {
console.error("JSON扁平化失败:", e);
return [];
}
}
const deepArray = [1, [2, 3], [4, [5, 6]]];
console.log("JSON扁平化:", flattenWithJSON(deepArray));
const mixedArray = [1, 'a', [2, null], [3, [4, undefined]]];
console.log("JSON扁平化 (混合数据):", flattenWithJSON(mixedArray));
const funcArray = [1, [2, () => {}]];
console.log("JSON扁平化 (含函数):", flattenWithJSON(funcArray));
巨大陷阱与不适用场景:
这种方法有很多严重的局限性,在绝大多数实际项目中都不推荐使用:
- 数据类型丢失或转换:
undefined、函数 (functions)、Symbol类型的值在JSON.stringify()后会被忽略或变为null,导致数据丢失。Date对象会转换为ISO格式的字符串,而不是Date对象本身。- 对象不会被扁平化,它们会作为完整的对象元素保留在数组中。
- 性能极差:字符串化和解析JSON,以及字符串替换操作,都是非常耗费性能的。
- 安全性问题:如果输入的数组包含循环引用,
JSON.stringify()会抛出错误。 - 只适用于扁平化数字、字符串、布尔值和
null组成的数组,且不包含对象。
结论: 了解这种方法作为一种“奇技淫巧”即可,但在实际生产环境中,请避免使用。
4. 性能对比与基准测试
理解了各种实现方式后,我们最关心的问题之一就是它们的性能表现。我们将通过编写基准测试代码来对比不同方法在不同场景下的性能。
4.1. 基准测试方法论
为了进行公平且有意义的性能对比,我们需要:
- 统一的测试数据:生成不同规模(大小)、不同深度(扁平、中等深度、超深)的测试数组。
- 多次运行取平均值:由于JavaScript引擎的JIT编译和垃圾回收机制,单次运行的结果可能不稳定。多次运行并计算平均时间可以得到更可靠的数据。
- 避免测量开销:确保测量的是函数本身的执行时间,而不是测试框架或数据生成的时间。
- 使用高精度计时器:
performance.now()比Date.now()更适合测量短时间内的代码执行。
4.2. 生成测试数据
我们将创建一些辅助函数来生成不同类型的嵌套数组。
/**
* 生成一个指定深度和大小的嵌套数组
* 该函数旨在创建结构复杂但元素总数可控的数组,以模拟真实场景
* @param {number} depth 数组的嵌套深度 (最小为1)
* @param {number} baseSize 每一层数组的基础元素数量
* @param {number} totalElements 扁平化后总元素数量的近似值
* @returns {Array} 生成的嵌套数组
*/
function generateNestedArray(depth, baseSize, totalElements) {
if (depth <= 0) {
return Array.from({ length: totalElements }, (_, i) => i);
}
const arr = [];
let currentTotal = 0;
// 填充当前层级的基本元素
for (let i = 0; i < baseSize && currentTotal < totalElements; i++) {
arr.push(i);
currentTotal++;
}
if (depth > 1) {
// 计算子数组应分摊的元素数量
const remainingElements = totalElements - currentTotal;
const numSubArrays = Math.min(depth - 1, baseSize); // 确保子数组数量合理
const elementsPerSubArray = numSubArrays > 0 ? Math.floor(remainingElements / numSubArrays) : 0;
for (let i = 0; i < numSubArrays && elementsPerSubArray > 0; i++) {
const subArrayDepth = Math.floor(Math.random() * (depth - 1)) + 1; // 随机子数组深度
const subArraySize = Math.max(1, Math.floor(baseSize / 2)); // 确保子数组有一定宽度
const subArray = generateNestedArray(subArrayDepth, subArraySize, elementsPerSubArray);
arr.push(subArray);
currentTotal += subArray.flat(Infinity).length; // 实际增加了多少元素
}
}
// 确保最终元素数量接近totalElements,并打乱顺序
while (arr.flat(Infinity).length < totalElements) {
arr.push(Math.random());
}
arr.sort(() => Math.random() - 0.5); // 打乱顺序
return arr;
}
// 浅层大数组 (深度1,总元素100,000)
const shallowLargeArray = generateNestedArray(1, 100000, 100000);
// 中等深度中等大小数组 (深度5,每层基础100个元素,总计约5,000元素)
const mediumDeepMediumArray = generateNestedArray(5, 100, 5000);
// 深层小数组 (深度100,每层基础1个元素,总计约100元素)
const deepSmallArray = generateNestedArray(100, 1, 100);
// 超深数组 (深度5000,每层基础1个元素,总计约5000元素)
// 递归方法可能会在此数据量下栈溢出
const ultraDeepArray = generateNestedArray(5000, 1, 5000);
// 超大数组 (深度5,总元素1,000,000)
const superLargeArray = generateNestedArray(5, 1000, 1000000);
4.3. 基准测试函数
/**
* 执行基准测试
* @param {string} name 测试名称
* @param {Function} func 要测试的扁平化函数
* @param {Array} data 测试数据
* @param {number} iterations 运行次数
*/
function benchmark(name, func, data, iterations = 100) {
const times = [];
let result = null;
let successfulIterations = 0;
// 预热JIT编译器
for (let i = 0; i < 5; i++) {
try {
func(data);
} catch (e) {
// 预热失败,忽略
}
}
for (let i = 0; i < iterations; i++) {
const start = performance.now();
try {
result = func(data);
const end = performance.now();
times.push(end - start);
successfulIterations++;
} catch (e) {
// console.warn(`Function '${name}' failed on iteration ${i} with error: ${e.message}`);
result = null; // 标记为失败
break; // 停止进一步迭代
}
}
if (successfulIterations === 0) {
return { name, avgTime: 'Error', minTime: 'Error', maxTime: 'Error', dataLength: 'N/A' };
}
const totalTime = times.reduce((sum, t) => sum + t, 0);
const avgTime = totalTime / successfulIterations;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
return { name, avgTime, minTime, maxTime, dataLength: result ? result.length : 'N/A' };
}
// 扁平化函数集合
const flatteningFunctions = {
'Array.prototype.flat(Infinity)': (arr) => arr.flat(Infinity),
'Recursive': flattenRecursive,
'Iterative (Optimized)': flattenIterativeOptimized,
'Reduce + Concat': flattenWithReduce,
'While + Some': flattenWithWhileAndSome,
'JSON.stringify/parse (Caution!)': flattenWithJSON
};
// 运行所有测试
console.log("--- 数组扁平化性能对比 ---");
const testResults = [];
function runScenario(scenarioName, data, iterations = 100) {
console.log(`n--- Scenario: ${scenarioName} ---`);
for (const funcName in flatteningFunctions) {
if (Object.hasOwnProperty.call(flatteningFunctions, funcName)) {
const func = flatteningFunctions[funcName];
const result = benchmark(funcName, func, data, iterations);
testResults.push({ scenario: scenarioName, ...result });
console.log(`${result.name}: Avg: ${result.avgTime === 'Error' ? 'Error' : result.avgTime.toFixed(3) + 'ms'}, Min: ${result.minTime === 'Error' ? 'Error' : result.minTime.toFixed(3) + 'ms'}, Max: ${result.maxTime === 'Error' ? 'Error' : result.maxTime.toFixed(3) + 'ms'}`);
}
}
}
runScenario('浅层大数组 (10万元素, 深度1)', shallowLargeArray);
runScenario('中等深度中等数组 (5千元素, 深度5)', mediumDeepMediumArray);
runScenario('深层小数组 (1百元素, 深度100)', deepSmallArray);
runScenario('超深数组 (5千元素, 深度5千)', ultraDeepArray, 10); // 减少迭代次数,因为可能耗时或失败
runScenario('超大数组 (100万元素, 深度5)', superLargeArray, 20); // 减少迭代次数
4.4. 性能测试结果分析 (理论与实践)
以下表格展示了在典型浏览器或Node.js环境下运行上述基准测试可能得到的理论结果。实际结果会因JS引擎版本、硬件环境、数组内容(纯数字、字符串、对象等)以及JIT优化程度而异。
性能对比表 (理论值,时间单位:毫秒 ms)
| 方法名称 | 场景: 浅层大数组 (10万元素, 深度1) | 场景: 中等深度中等数组 (5千元素, 深度5) | 场景: 深层小数组 (1百元素, 深度100) | 场景: 超深数组 (5千元素, 深度5千) | 场景: 超大数组 (100万元素, 深度5) | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|
Array.prototype.flat(Infinity) |
0.5 – 2 ms | 0.1 – 0.5 ms | 0.01 – 0.1 ms | 0.5 – 2 ms | 5 – 20 ms | 极速、内置、简洁、声明式 | 兼容性 (ES2019), 无法自定义扁平化逻辑 |
Recursive |
1 – 5 ms | 0.2 – 1 ms | 0.05 – 0.2 ms | Error (栈溢出) | 10 – 50 ms | 直观、易于理解 | 栈溢出风险高、性能受调用栈深度影响 |
Iterative (Optimized) |
1 – 5 ms | 0.2 – 1 ms | 0.03 – 0.15 ms | 0.2 – 1 ms | 10 – 40 ms | 避免栈溢出、性能稳定 | 代码稍复杂、需要 reverse() 操作 |
Reduce + Concat |
2 – 10 ms | 0.5 – 2 ms | 0.1 – 0.5 ms | Error (栈溢出) | 20 – 100 ms | 函数式、简洁 | 栈溢出风险、频繁 concat 性能开销 |
While + Some |
5 – 20 ms | 1 – 5 ms | 0.5 – 2 ms | 5 – 20 ms | 50 – 200 ms | 易理解 |