数组扁平化怎么写?多种JavaScript实现方式与性能对比

各位技术同仁,大家好。今天我们将深入探讨一个在JavaScript编程中非常常见,却又充满技巧性的话题:数组扁平化(Array Flattening)。在处理复杂数据结构,特别是从API获取的嵌套数据、用户界面中的层级组件状态,或者图形算法的遍历结果时,我们经常会遇到需要将多维数组转换为一维数组的需求。理解并掌握多种数组扁平化技术,不仅能提升我们的代码效率,更能拓宽解决问题的思路。

本次讲座,我将作为一名编程专家,带领大家从概念到实现,从基础到高级,再到性能对比,全面剖析数组扁平化的方方面面。我们将考察不同的JavaScript实现方式,包括ES2019中引入的内置方法,以及各种手动实现策略,并对它们的性能进行深入对比,最终帮助大家在实际项目中做出明智的技术选型。


1. 数组扁平化的核心概念与应用场景

什么是数组扁平化?

数组扁平化,顾名思义,就是将一个包含多层嵌套数组的复杂结构,转换成一个只有一层(一维)的数组。例如,我们可能有一个这样的数组:[1, [2, 3], [4, [5, 6]]]。经过扁平化处理后,它应该变成 [1, 2, 3, 4, 5, 6]

扁平化操作的关键在于如何处理数组中的数组元素。有些时候,我们可能只关心扁平化一层嵌套(浅扁平化),而另一些时候,我们需要将所有层级的嵌套都展开(深扁平化)。

为什么需要数组扁平化?

数组扁平化在实际开发中有着广泛的应用:

  1. 数据处理与分析:当从后端API获取的数据是嵌套结构时,为了方便统一处理(如过滤、映射、查找),我们常常需要先将其扁平化。例如,一个订单可能包含多个商品,每个商品又包含多个SKU,如果我们想统计所有SKU的总数量,扁平化数据会使操作更直接。
  2. UI渲染:在前端框架(如React、Vue)中,有时组件的子元素是动态生成的,并且可能以嵌套数组的形式返回。为了将其渲染为单一的列表或网格,扁平化是必要的。
  3. 算法与数据结构:在图遍历(如深度优先搜索或广度优先搜索)或树结构的处理中,结果往往是嵌套的。扁平化可以帮助我们统一收集所有节点。
  4. 简化数据结构:扁平化可以降低数据结构的复杂性,使代码逻辑更清晰,减少迭代嵌套数组时的心智负担。

理解了这些背景,我们就可以开始探索具体的实现方式了。


2. 基础扁平化:内置方法与浅层扁平化

在JavaScript中,处理数组扁平化已经变得越来越简单,特别是随着ES2019的引入。

2.1. Array.prototype.flat():现代、声明式扁平化

Array.prototype.flat() 是ES2019中新增的数组方法,它为数组扁平化提供了一个非常简洁且强大的内置解决方案。这是目前在支持ES2019及以上环境中最推荐使用的方法。

语法:

arr.flat([depth])
  • depth (可选): 指定要扁平化的层数。默认值为 1
  • 如果 depthInfinity,则会递归地扁平化所有嵌套数组,直到数组中不再有任何嵌套数组。

示例:浅层扁平化 (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) 执行时:

  1. ...nestedArray[1, [2, 3], [4, [5, 6]]] 展开为 1, [2, 3], [4, [5, 6]]
  2. [].concat(1, [2, 3], [4, [5, 6]]) 将这些元素连接到一个空数组 [] 上。
  3. concat 方法会将非数组元素直接添加,对于数组元素,它会将其展开一层。所以 [2, 3] 变成了 2, 3,而 [4, [5, 6]] 也被展开成了 4, [5, 6]

局限性:

这种方法只能进行浅层扁平化。如果数组嵌套层级超过一层,它将无法完全扁平化。例如,[4, [5, 6]] 在扁平化后仍然是 [4, [5, 6]],内层的 [5, 6] 保持不变。因此,对于深层扁平化,我们需要更复杂的策略。


3. 深度扁平化:手动实现策略与高级技巧

flat() 方法不适用(例如,需要兼容旧环境)或需要对扁平化过程进行更细粒度的控制时,我们可以采用多种手动实现的策略来实现深度扁平化。

3.1. 递归实现:经典而直观

递归是处理树形或嵌套结构最自然的方式之一。对于数组扁平化,其核心思想是:遍历数组中的每个元素。如果元素是数组,则对其递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。

实现思路:

  1. 创建一个空数组 result 用于存放扁平化后的元素。
  2. 遍历输入数组的每个元素。
  3. 对于每个元素:
    • 如果它是数组,则递归调用扁平化函数,并将返回的结果通过 concatpush(... 方式添加到 result 中。
    • 如果它不是数组,则直接将其添加到 result 中。
  4. 返回 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. 迭代实现(使用栈):避免栈溢出

为了避免递归带来的栈溢出问题,我们可以采用迭代的方式来实现深度扁平化。这种方法通常使用一个辅助栈(或者直接操作原数组)来管理待处理的元素。

实现思路:

  1. 创建一个空数组 result 用于存放扁平化后的元素。
  2. 创建一个栈 stack,并将输入数组的元素以逆序推入栈中(这样在弹出时可以保持原有的顺序)。
  3. 当栈不为空时,循环执行以下操作:
    • 从栈顶弹出一个元素。
    • 如果该元素是数组,则将其所有子元素(再次以逆序)推入栈中。
    • 如果该元素不是数组,则将其添加到 result 数组的开头(因为我们是逆序处理,所以需要添加到开头才能保持原序)。
  4. 返回 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()):

上述迭代方法需要将非数组元素 unshiftresult 数组的开头,这可能会导致性能问题(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(),我们可以实现一种优雅的深度扁平化。

实现思路:

  1. 使用 reduce() 遍历数组的每个元素。
  2. reduce() 的回调函数接收一个累加器 acc 和当前元素 cur
  3. 如果 cur 是一个数组,则递归调用扁平化函数(或再次使用 reduce()),然后将结果与 acc 合并(acc.concat(...))。
  4. 如果 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() 和展开运算符来实现每一层的扁平化。

实现思路:

  1. 初始化 result 为输入数组的一个副本。
  2. 进入一个 while 循环,条件是 result 中是否存在任何数组元素(可以使用 result.some(Array.isArray) 来检查)。
  3. 在循环内部,使用 [].concat(...result)result 数组扁平化一层。
  4. 当循环结束时,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序列化和反序列化的特性。

原理:

  1. 将嵌套数组 JSON.stringify() 转换为字符串。这会将所有数组都变成 [...] 形式的字符串。
  2. 然后,使用正则表达式将字符串中的 [] 替换掉,只留下数字和逗号。
  3. 最后,将处理过的字符串通过 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,丢失!

巨大陷阱与不适用场景:

这种方法有很多严重的局限性,在绝大多数实际项目中都不推荐使用

  1. 数据类型丢失或转换
    • undefined、函数 (functions)、Symbol 类型的值在 JSON.stringify() 后会被忽略或变为 null,导致数据丢失。
    • Date 对象会转换为ISO格式的字符串,而不是 Date 对象本身。
    • 对象不会被扁平化,它们会作为完整的对象元素保留在数组中。
  2. 性能极差:字符串化和解析JSON,以及字符串替换操作,都是非常耗费性能的。
  3. 安全性问题:如果输入的数组包含循环引用,JSON.stringify() 会抛出错误。
  4. 只适用于扁平化数字、字符串、布尔值和 null 组成的数组,且不包含对象。

结论: 了解这种方法作为一种“奇技淫巧”即可,但在实际生产环境中,请避免使用


4. 性能对比与基准测试

理解了各种实现方式后,我们最关心的问题之一就是它们的性能表现。我们将通过编写基准测试代码来对比不同方法在不同场景下的性能。

4.1. 基准测试方法论

为了进行公平且有意义的性能对比,我们需要:

  1. 统一的测试数据:生成不同规模(大小)、不同深度(扁平、中等深度、超深)的测试数组。
  2. 多次运行取平均值:由于JavaScript引擎的JIT编译和垃圾回收机制,单次运行的结果可能不稳定。多次运行并计算平均时间可以得到更可靠的数据。
  3. 避免测量开销:确保测量的是函数本身的执行时间,而不是测试框架或数据生成的时间。
  4. 使用高精度计时器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

  1. Introduction: The Concept of Array Flattening
  2. Basic Implementations (Shallow Flattening)
    2.1. Array.prototype.flat() (ES2019)
    2.2. concat() and Spread Operator (...)
  3. Deep Flattening Implementations (Pre-ES2019 & Manual Approaches)
    3.1. Recursive Approach
    3.2. Iterative Approach (using a stack)
    3.3. reduce() and concat()
    3.4. while loop and some()
    3.5. JSON.stringify() and JSON.parse() (Hack/Trick, with caveats)
  4. Performance Comparison and Benchmarking
    4.1. Methodology
    4.2. Generating Test Data
    4.3. Benchmarking Function
    4.4. Performance Test Results Analysis
  5. Practical Applications and Choosing the Right Method
  6. Best Practices and Potential Pitfalls
  7. Concluding Thoughts

1. 数组扁平化的核心概念与应用场景

数组扁平化,顾名思义,就是将一个包含多层嵌套数组的复杂结构,转换成一个只有一层(一维)的数组。例如,我们可能有一个这样的数组:[1, [2, 3], [4, [5, 6]]]。经过扁平化处理后,它应该变成 [1, 2, 3, 4, 5, 6]

扁平化操作的关键在于如何处理数组中的数组元素。有些时候,我们可能只关心扁平化一层嵌套(浅扁平化),而另一些时候,我们需要将所有层级的嵌套都展开(深扁平化)。

为什么需要数组扁平化?

数组扁平化在实际开发中有着广泛的应用:

  1. 数据处理与分析:当从后端API获取的数据是嵌套结构时,为了方便统一处理(如过滤、映射、查找),我们常常需要先将其扁平化。例如,一个订单可能包含多个商品,每个商品又包含多个SKU,如果我们想统计所有SKU的总数量,扁平化数据会使操作更直接。
  2. UI渲染:在前端框架(如React、Vue)中,有时组件的子元素是动态生成的,并且可能以嵌套数组的形式返回。为了将其渲染为单一的列表或网格,扁平化是必要的。
  3. 算法与数据结构:在图遍历(如深度优先搜索或广度优先搜索)或树结构的处理中,结果往往是嵌套的。扁平化可以帮助我们统一收集所有节点。
  4. 简化数据结构:扁平化可以降低数据结构的复杂性,使代码逻辑更清晰,减少迭代嵌套数组时的心智负担。

理解了这些背景,我们就可以开始探索具体的实现方式了。


2. 基础扁平化:内置方法与浅层扁平化

在JavaScript中,处理数组扁平化已经变得越来越简单,特别是随着ES2019的引入。

2.1. Array.prototype.flat():现代、声明式扁平化

Array.prototype.flat() 是ES2019中新增的数组方法,它为数组扁平化提供了一个非常简洁且强大的内置解决方案。这是目前在支持ES2019及以上环境中最推荐使用的方法。

语法:

arr.flat([depth])
  • depth (可选): 指定要扁平化的层数。默认值为 1
  • 如果 depthInfinity,则会递归地扁平化所有嵌套数组,直到数组中不再有任何嵌套数组。

示例:浅层扁平化 (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) 执行时:

  1. ...nestedArray[1, [2, 3], [4, [5, 6]]] 展开为 1, [2, 3], [4, [5, 6]]
  2. [].concat(1, [2, 3], [4, [5, 6]]) 将这些元素连接到一个空数组 [] 上。
  3. concat 方法会将非数组元素直接添加,对于数组元素,它会将其展开一层。所以 [2, 3] 变成了 2, 3,而 [4, [5, 6]] 也被展开成了 4, [5, 6]

局限性:

这种方法只能进行浅层扁平化。如果数组嵌套层级超过一层,它将无法完全扁平化。例如,[4, [5, 6]] 在扁平化后仍然是 [4, [5, 6]],内层的 [5, 6] 保持不变。因此,对于深层扁平化,我们需要更复杂的策略。


3. 深度扁平化:手动实现策略与高级技巧

flat() 方法不适用(例如,需要兼容旧环境)或需要对扁平化过程进行更细粒度的控制时,我们可以采用多种手动实现的策略来实现深度扁平化。

3.1. 递归实现:经典而直观

递归是处理树形或嵌套结构最自然的方式之一。对于数组扁平化,其核心思想是:遍历数组中的每个元素。如果元素是数组,则对其递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。

实现思路:

  1. 创建一个空数组 result 用于存放扁平化后的元素。
  2. 遍历输入数组的每个元素。
  3. 对于每个元素:
    • 如果它是数组,则递归调用扁平化函数,并将返回的结果通过 concatpush(... 方式添加到 result 中。
    • 如果它不是数组,则直接将其添加到 result 中。
  4. 返回 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. 迭代实现(使用栈):避免栈溢出

为了避免递归带来的栈溢出问题,我们可以采用迭代的方式来实现深度扁平化。这种方法通常使用一个辅助栈(或者直接操作原数组)来管理待处理的元素。

实现思路:

  1. 创建一个空数组 result 用于存放扁平化后的元素。
  2. 创建一个栈 stack,并将输入数组的元素以逆序推入栈中(这样在弹出时可以保持原有的顺序)。
  3. 当栈不为空时,循环执行以下操作:
    • 从栈顶弹出一个元素。
    • 如果该元素是数组,则将其所有子元素(再次以逆序)推入栈中。
    • 如果该元素不是数组,则将其添加到 result 数组的开头(因为我们是逆序处理,所以需要添加到开头才能保持原序)。
  4. 返回 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(),我们可以实现一种优雅的深度扁平化。

实现思路:

  1. 使用 reduce() 遍历数组的每个元素。
  2. reduce() 的回调函数接收一个累加器 acc 和当前元素 cur
  3. 如果 cur 是一个数组,则递归调用扁平化函数(或再次使用 reduce()),然后将结果与 acc 合并(acc.concat(...))。
  4. 如果 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() 和展开运算符来实现每一层的扁平化。

实现思路:

  1. 初始化 result 为输入数组的一个副本。
  2. 进入一个 while 循环,条件是 result 中是否存在任何数组元素(可以使用 result.some(Array.isArray) 来检查)。
  3. 在循环内部,使用 [].concat(...result)result 数组扁平化一层。
  4. 当循环结束时,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序列化和反序列化的特性。

原理:

  1. 将嵌套数组 JSON.stringify() 转换为字符串。这会将所有数组都变成 [...] 形式的字符串。
  2. 然后,使用正则表达式将字符串中的 [] 替换掉,只留下数字和逗号。
  3. 最后,将处理过的字符串通过 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));

巨大陷阱与不适用场景:

这种方法有很多严重的局限性,在绝大多数实际项目中都不推荐使用

  1. 数据类型丢失或转换
    • undefined、函数 (functions)、Symbol 类型的值在 JSON.stringify() 后会被忽略或变为 null,导致数据丢失。
    • Date 对象会转换为ISO格式的字符串,而不是 Date 对象本身。
    • 对象不会被扁平化,它们会作为完整的对象元素保留在数组中。
  2. 性能极差:字符串化和解析JSON,以及字符串替换操作,都是非常耗费性能的。
  3. 安全性问题:如果输入的数组包含循环引用,JSON.stringify() 会抛出错误。
  4. 只适用于扁平化数字、字符串、布尔值和 null 组成的数组,且不包含对象。

结论: 了解这种方法作为一种“奇技淫巧”即可,但在实际生产环境中,请避免使用


4. 性能对比与基准测试

理解了各种实现方式后,我们最关心的问题之一就是它们的性能表现。我们将通过编写基准测试代码来对比不同方法在不同场景下的性能。

4.1. 基准测试方法论

为了进行公平且有意义的性能对比,我们需要:

  1. 统一的测试数据:生成不同规模(大小)、不同深度(扁平、中等深度、超深)的测试数组。
  2. 多次运行取平均值:由于JavaScript引擎的JIT编译和垃圾回收机制,单次运行的结果可能不稳定。多次运行并计算平均时间可以得到更可靠的数据。
  3. 避免测量开销:确保测量的是函数本身的执行时间,而不是测试框架或数据生成的时间。
  4. 使用高精度计时器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 易理解

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注