数组扁平化的详解

一、数组扁平化的定义(速记版)

**面试速记:数组扁平化(Flatten Array)指将嵌套的多维数组转换为一维数组的操作,核心是拆解数组的嵌套层级,让所有元素都处于同一层级。**例如:
  • 多维数组:[1, [2, [3, 4], 5], 6]
  • 扁平化后:[1, 2, 3, 4, 5, 6]

应用场景

  1. 日常开发 :优先用 Array.prototype.flat(Infinity)(原生高效)或 Lodash(复杂场景);

  2. 面试手写:掌握「递归法」和「reduce + 递归」,进阶可提「尾递归优化」或「Generator」;

  3. 临时测试:用「JSON.stringify + JSON.parse」或「正则 + split」(仅限简单数组);

  4. 大数据 / 惰性场景:用 Generator 生成器(按需迭代,减少内存占用)。

性能比较
递归方法:通用但可能栈溢出
flat方法:性能最佳但需现代环境
toString方法:仅限数字数组且效率较低
总结:每种方法各有优劣,选择时应考虑浏览器兼容性、数据特点和性能需求。现代项目推荐优先使用原生flat方法,旧项目可采用reduce或递归方案。

二、详解常见实现方式(原理)

方式 1:ES6 原生 flat() 方法(推荐,最简洁)

Array.prototype.flat(depth) 是 ES2019 新增的专用扁平化方法,无需手动实现核心逻辑:

  • 参数 depth:可选,指定扁平化深度(默认 1);传入 Infinity 可处理任意深度的嵌套数组;
  • 返回值:新的一维数组(不修改原数组)。

javascript

运行

javascript 复制代码
// 基础示例
const nestedArr = [1, [2, [3, 4], 5], 6];

// 扁平化1层(默认行为)
const flat1Layer = nestedArr.flat(); 
console.log(flat1Layer); // [1, 2, [3, 4], 5, 6]

// 扁平化无限层级(最常用)
const flatAll = nestedArr.flat(Infinity); 
console.log(flatAll); // [1, 2, 3, 4, 5, 6]

// 自动忽略空项(额外特性)
const arrWithEmpty = [1, , [2, [3]]];
console.log(arrWithEmpty.flat(Infinity)); // [1, 2, 3]

解释

  • flat() 无参数时仅拆解一层嵌套;
  • flat(Infinity) 是实战中最常用的写法,适配所有嵌套深度;
  • 该方法会自动过滤数组中的空元素(如 [,])。
方式 2:手动递归实现(理解核心逻辑)

核心思路:遍历数组元素,若元素是数组则递归调用扁平化函数,否则直接加入结果数组(面试高频考点)。

javascript

运行

javascript 复制代码
function flattenArray(arr) {
  // 复制原数组,避免修改原数据
  let tempArr = [...arr];
  
  // 循环判断:数组中是否还有嵌套的数组
  while (tempArr.some(item => Array.isArray(item))) {
    // 扩展运算符拆解一层,重新合并为新数组
    tempArr = [].concat(...tempArr);
  }
  
  return tempArr;
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]

解释

  • 递归终止条件:元素不是数组时,直接 push 到结果;
  • concat() 用于合并递归返回的扁平化数组,避免修改原结果数组;
  • 相比 typeof item === 'object'Array.isArray() 能准确判断数组(排除 null / 对象等干扰)。
方式 3:reduce + 递归(函数式简洁写法)

利用 reduce 的累加器特性替代手动循环,代码更简洁,符合函数式编程风格:

javascript

运行

javascript 复制代码
function flattenArray(arr) {
  // reduce 遍历数组,acc 是累加器(最终一维数组),item 是当前元素
  return arr.reduce((acc, item) => {
    // 数组元素递归扁平化,非数组元素直接返回
    return acc.concat(Array.isArray(item) ? flattenArray(item) : item);
  }, []); // 累加器初始值为空数组
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]

解释

  • reduce 回调的核心逻辑与递归法一致,仅用 reduce 替代了 for...of 循环;
  • 初始值 [] 确保累加器从空数组开始,避免空指针问题。
方式 4:扩展运算符 + 循环(逐层拆解)

利用扩展运算符(...)拆解数组的第一层嵌套,循环直到无嵌套数组:

javascript

运行

javascript 复制代码
function flattenArray(arr) {
  // 复制原数组,避免修改原数据
  let tempArr = [...arr];
  
  // 循环判断:数组中是否还有嵌套的数组
  while (tempArr.some(item => Array.isArray(item))) {
    // 扩展运算符拆解一层,重新合并为新数组
    tempArr = [].concat(...tempArr);
  }
  
  return tempArr;
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]

解释

  • some() 检查数组中是否存在数组类型的元素,存在则继续循环;
  • [].concat(...tempArr) 会将每一层数组 "展开"(如 [1, [2,3]][1,2,3]);
  • 循环终止条件:some() 返回 false(无嵌套数组)。

三、其它方法

一、JSON.stringify + JSON.parse(字符串拆解法)

核心思路

利用 JSON.stringify() 将多维数组转为无嵌套的字符串 (自动去掉数组括号),再通过正则替换掉所有括号,最后用 JSON.parse() 转回数组。

代码实现

javascript

运行

javascript 复制代码
function flattenArray(arr) {
  // 1. 转为JSON字符串(如[1,[2,[3]]] → "[1,[2,[3]]]")
  const str = JSON.stringify(arr);
  // 2. 正则替换所有[]为空,得到纯元素字符串(→ "1,2,3")
  const flatStr = str.replace(/\[|\]/g, '');
  // 3. 转回数组(注意:空数组需特殊处理)
  return flatStr ? JSON.parse(`[${flatStr}]`) : [];
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]

// 空数组/空元素测试
console.log(flattenArray([])); // []
console.log(flattenArray([[], [[]]])); // []
解释与优缺点
  • 核心逻辑 :JSON.stringify 会将数组的嵌套结构转为字符串形式,正则 /\[|\]/g 匹配所有方括号并删除,最终还原为一维数组字符串。
  • 优点:代码极简,无需递归 / 循环,适合快速实现;
  • 缺点
    1. 不支持包含 undefined、function、Symbol、BigInt 的数组(JSON.stringify 会忽略 / 报错这些类型);
    2. 若元素本身包含 [/] 字符(如字符串 "[123]"),会被错误替换,导致结果异常;
    3. 所有数字会被转为字符串再转回数字,少量性能损耗。
  • 适用场景 :数组元素仅为数字、字符串、布尔值、null 等基础类型,且无特殊字符的简单场景。

二、forEach + 递归(遍历简化版)

核心思路

本质是「手动递归」的简化写法,用 Array.prototype.forEach 替代 for...of 循环,遍历逻辑更简洁,无需手动维护索引。

代码实现

javascript

运行

javascript 复制代码
function flattenArray(arr, result = []) {
  // forEach 遍历每个元素,自动跳过空项
  arr.forEach(item => {
    if (Array.isArray(item)) {
      // 递归处理子数组,复用同一个result数组(减少内存开销)
      flattenArray(item, result);
    } else {
      result.push(item);
    }
  });
  return result;
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
  • 核心逻辑 :通过「默认参数 result = []」复用同一个结果数组,避免每次递归都创建新数组(相比 concat 更高效);
  • 优点:代码简洁、内存效率高,无循环索引管理;
  • 缺点 :依赖递归,嵌套层级极深时(如 10000 层)会触发 Maximum call stack size exceeded 栈溢出;
  • 适用场景:中等嵌套深度的数组,追求代码简洁性。

三、Generator 生成器(迭代器模式)

核心思路

利用 ES6 Generator 函数的「惰性迭代」特性,逐个遍历元素,遇到数组则递归 yield* 展开,最终将迭代器转为数组。

代码实现

javascript

运行

javascript 复制代码
// 定义生成器函数
function* flattenGenerator(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      // yield* 递归迭代子数组的生成器
      yield* flattenGenerator(item);
    } else {
      // 逐个产出非数组元素
      yield item;
    }
  }
}

// 封装为扁平化函数
function flattenArray(arr) {
  // 将生成器转为数组([...iterator] 或 Array.from)
  return [...flattenGenerator(arr)];
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
  • 核心逻辑 :Generator 函数通过 yield 产出单个元素,yield* 可迭代另一个生成器(实现递归展开),最终通过扩展运算符消费迭代器得到一维数组;
  • 优点:惰性迭代(可按需获取元素,无需一次性生成完整数组)、符合迭代器设计模式;
  • 缺点 :语法稍复杂,性能略低于原生 flat()
  • 适用场景:需要惰性处理大数据量数组、或结合迭代器 / 异步场景的开发。

四、第三方库(Lodash):.flattenDeep/.flattenDepth

核心思路

实际开发中,若需兼容低版本环境或处理复杂边界(如空值、特殊类型),优先使用成熟的第三方库(如 Lodash),其封装了完善的扁平化方法。

代码实现

javascript

运行

javascript 复制代码
// 先安装:npm i lodash
const _ = require('lodash');

const nestedArr = [1, [2, [3, 4], 5], 6];

// 扁平化所有层级(等价于 flat(Infinity))
console.log(_.flattenDeep(nestedArr)); // [1, 2, 3, 4, 5, 6]

// 指定扁平化深度(等价于 flat(2))
console.log(_.flattenDepth(nestedArr, 2)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
  • 核心逻辑:Lodash 内部通过优化的递归 / 循环实现,处理了空项、特殊类型(如类数组、Symbol)等边界情况;
  • 优点:兼容性强、鲁棒性高、支持深度配置;
  • 缺点 :需引入第三方库(增加体积,可按需引入 lodash/flattenDeep);
  • 适用场景:企业级项目、需兼容多环境、处理复杂数组场景。

五、正则表达式 + split(极简字符串法)

核心思路

直接将数组转为字符串,用正则去掉所有括号后按逗号分割,再转为数组(比 JSON.stringify 更直接,但类型转换需注意)。

代码实现

javascript

运行

javascript 复制代码
function flattenArray(arr) {
  // 转为字符串 → 去掉所有[] → 按逗号分割 → 过滤空字符串 → 转数字(按需)
  return arr.toString()
    .replace(/\[|\]/g, '')
    .split(',')
    .filter(item => item) // 过滤空项(如[,]产生的空字符串)
    .map(item => Number(item)); // 若元素是数字,转回数字类型
}

// 测试
const nestedArr = [1, [2, [3, 4], 5], 6];
console.log(flattenArray(nestedArr)); // [1, 2, 3, 4, 5, 6]
解释与优缺点
  • 核心逻辑arr.toString() 会将多维数组转为 1,2,3,4,5,6 形式(自动去掉括号),后续通过字符串处理还原数组;
  • 优点:代码最短,适合快速测试;
  • 缺点
    1. 所有元素会被转为字符串(需手动转回原类型);
    2. 若元素包含逗号(如字符串 "a,b"),会被错误分割;
  • 适用场景:快速验证、元素为纯数字且无特殊字符的临时场景。

六、补充:尾递归优化(解决栈溢出)

针对递归法的「栈溢出」问题,可通过尾递归优化(ES6 严格模式支持),让递归调用在栈帧末尾执行,避免栈溢出。

代码实现

javascript

运行

javascript 复制代码
// 严格模式启用尾递归优化
'use strict';

function flattenArray(arr, result = [], index = 0) {
  // 遍历完成,返回结果
  if (index >= arr.length) return result;
  
  const item = arr[index];
  if (Array.isArray(item)) {
    // 尾递归:处理子数组,index重置为0
    return flattenArray(item, result, 0);
  } else {
    // 非数组元素加入结果,index+1继续遍历
    result.push(item);
    return flattenArray(arr, result, index + 1);
  }
}

// 测试(深层嵌套也不会栈溢出)
const deepArr = [1, [2, [3, [4, [5]]]]];
console.log(flattenArray(deepArr)); // [1, 2, 3, 4, 5]
核心逻辑

尾递归将「遍历索引」和「结果数组」作为参数传递,递归调用是函数最后一步操作,引擎可复用栈帧,避免栈溢出。

七、新增方法总结对比

实现方式 核心特点 优点 缺点
JSON.stringify + JSON.parse 字符串拆解 极简 不支持特殊类型、易被特殊字符干扰
forEach + 递归 遍历简化、复用结果数组 简洁、内存高效 嵌套过深栈溢出
Generator 生成器 惰性迭代、迭代器模式 按需取值、适配异步场景 语法复杂、性能略低
Lodash _.flattenDeep 成熟封装、边界处理完善 兼容强、鲁棒性高 引入第三方库
正则 + split 字符串分割 代码最短 类型转换问题、易被逗号干扰
尾递归优化 解决栈溢出 支持深层嵌套 仅 ES6 严格模式生效

八、总结

实现方式 优点 适用场景
flat(Infinity) 简洁、原生、高性能 日常开发(ES6+ 环境)
手动递归 理解核心逻辑 面试手写、低版本环境兼容
reduce + 递归 简洁、函数式风格 代码精简需求、函数式编程
扩展运算符 + 循环 直观(逐层拆解) 理解扁平化过程的教学场景

日常开发优先使用 flat(Infinity);面试中需掌握递归 /reduce 实现,理解 "遍历 - 判断 - 递归 / 合并" 的核心逻辑。

相关推荐
柒儿吖4 小时前
官方适配完的命令行ruby在鸿蒙PC上的使用方法
开发语言·ruby·harmonyos
报错小能手4 小时前
STL——set
开发语言·c++
一勺菠萝丶4 小时前
执行 install.sh 报错 `env: ‘bash\r‘: No such file or directory` 怎么解决?
开发语言·bash
csbysj20204 小时前
MySQL UPDATE 更新操作详解
开发语言
YaeZed4 小时前
Vue3-动态组件
前端·vue.js
Yupureki4 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-双指针
c语言·开发语言·数据结构·c++·算法·visual studio
单身的人上天堂4 小时前
开发中使用iconfont预览太麻烦?我开发了一款VSCode插件来提升效率
前端·javascript·visual studio code
锥锋骚年4 小时前
golang 开发 Redis与Memory统一接口方案
开发语言·redis·golang
鹏多多4 小时前
前端项目package.json与package-lock.json的详细指南
前端·vue.js·react.js