前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库数组扁平化 手写 flat
前端手写题实用函数实现

怎么用递归手写一个数组扁平化函数?

遍历数组,遇到子数组就递归调用自己把它拍平,再把结果放进新数组;遇到普通值直接放进去。判断数组用 Array.isArray,整个过程不改原数组。

前端进阶之旅 · 一题精讲更新于 2026.09.29
前端手写题#实用函数实现#测试
先看核心答案读代码示例
理解线索

递归拍平的三个要点

  1. Array.isArray唯一可靠的数组判断,typeof 对数组返回 object
  2. 递归展开子数组拍平后的结果按顺序并入当前结果
  3. 新数组返回结果是新建的,原数组和它的子数组都不动

原生 flat 会跳过稀疏数组的空位,for...of 写的递归会把空位读成 undefined 保留下来,两者结果不一样。

核心回答

先记住这个答案

写一个 flatten(arr),先建一个空的 result,然后遍历 arr:当前元素是数组,就递归调用 flatten 把它拍平,再把得到的元素依次放进 result;不是数组就直接放进去。递归每次都返回一个一维数组,所以不管嵌套几层,最后都会被一层层剥开。判断数组要用 Array.isArray,函数返回新数组,不修改传进来的数组。

  • 用 Array.isArray 判断,是数组就递归,否则直接放入
  • 返回新数组,不修改原数组
  • 加一个 depth 参数就能对齐原生 flat
  • 嵌套极深会爆栈,改用显式栈循环

基础版:遇到数组就递归

先写出最短能跑的版本,再用几种输入测一下,包括空数组和混着对象的数组。

递归实现和测试JavaScript
function flatten(arr) {
  const result = []
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatten(item)) // 子数组拍平后逐个放进来
    } else {
      result.push(item)
    }
  }
  return result
}

console.log(JSON.stringify(flatten([1, [2, [3, [4]]], 5])))
console.log(JSON.stringify(flatten([[], [[]], 1])))
console.log(JSON.stringify(flatten([['a'], { b: [1] }, [['c']]])))
查看输出与解释
[1,2,3,4,5]
[1]
["a",{"b":[1]},"c"]

第二个用例里全是空数组,递归返回的都是空数组,展开后什么也不加,最后只剩 1。第三个用例里对象 { b: [1] } 不是数组,会原样放进结果,对象里面的数组也不会被拆开。扁平化只处理数组嵌套数组的情况。 result.push(...flatten(item)) 也可以写成 result = result.concat(flatten(item)),结果一样,只是 concat 每次都会再建一个新数组,递归层数多的时候多一些拷贝。

对齐原生 flat:加上 depth 参数

原生 arr.flat() 默认只拍平一层,arr.flat(Infinity) 才是全部拍平。面试官经常让你补上这个参数,改动很小:多传一个 depth,只有 depth 大于 0 时才递归,递归时减 1。

支持指定深度JavaScript
function flatten(arr, depth = 1) {
  const result = []
  for (const item of arr) {
    if (Array.isArray(item) && depth > 0) {
      result.push(...flatten(item, depth - 1))
    } else {
      result.push(item)
    }
  }
  return result
}

const nested = [1, [2, [3, [4]]]]
console.log(JSON.stringify(flatten(nested)))
console.log(JSON.stringify(flatten(nested, Infinity))) // Infinity 减 1 还是 Infinity
console.log(JSON.stringify(flatten([1, , [2]], Infinity))) // 稀疏数组
console.log(JSON.stringify([1, , [2]].flat(Infinity)))
查看输出与解释
[1,2,[3,[4]]]
[1,2,3,4]
[1,null,2]
[1,2]

Infinity - 1 仍然是 Infinity,所以传 Infinity 时每一层都会继续递归,不用单独处理。

稀疏数组的差别
[1, , [2]] 中间有一个空位,就是这个下标上根本没有元素。for...of 遍历时会把空位读成 undefined,所以手写版结果里多了一项(JSON.stringify 把 undefined 数组元素打印成 null)。原生 flat 会直接跳过空位。想完全对齐,可以用下标循环,并用 i in arr 判断这个位置有没有元素。

嵌套太深时改用显式栈

递归每深入一层就占一个调用栈帧,嵌套层数足够多时会抛 RangeError: Maximum call stack size exceeded。栈的上限和引擎、函数本身都有关,没有固定数字。数据来自用户或者程序生成的时候,可以把递归改成自己维护一个栈。

用栈代替递归JavaScript
function flattenIter(arr) {
  const stack = [...arr]
  const result = []
  while (stack.length) {
    const item = stack.pop() // 从末尾取,所以结果是倒序的
    if (Array.isArray(item)) stack.push(...item)
    else result.push(item)
  }
  return result.reverse()
}

let deep = [0]
for (let i = 0; i < 100000; i++) deep = [deep] // 嵌套十万层

console.log(JSON.stringify(flattenIter([1, [2, [3, [4]]], 5])))
console.log(flattenIter(deep).length)
查看输出与解释
[1,2,3,4,5]
1

栈放在堆内存里,层数再多也不会爆调用栈。因为每次从末尾 pop,元素是倒着进结果的,最后 reverse 一次恢复顺序。十万层的数组用递归版基本一定爆栈,用栈版能正常得到结果。

回答前,多想一步

容易答错的地方

用 typeof item === 'array' 判断数组
typeof 对数组返回的是 object,永远不会等于 array,这个判断一直是 false,结果就是什么都没拍平。要用 Array.isArray。item instanceof Array 在 iframe 之间传递数组时会判断错,因为不同窗口有各自的 Array 构造函数,Array.isArray 没有这个问题。
用 toString 或 join 偷懒
arr.toString().split(',') 看起来也能拍平,但所有元素都变成了字符串,数字 1 变成 '1',对象变成 [object Object],元素里本来就带逗号的字符串还会被切开。面试时可以提一句这个写法,但要说明它只适用于纯数字且不在乎类型的场景。
试着用自己的话回答

面试官还会怎么问?

能用 reduce 写一行吗?

可以:const flatten = (arr) => arr.reduce((acc, x) => acc.concat(Array.isArray(x) ? flatten(x) : x), [])。思路和循环版一样,只是用 reduce 做累加。每一步 concat 都会生成新数组,数据量大时比 push 慢一些。

数组里引用了自己会怎样?

比如 const a = [1]; a.push(a),递归版会一直展开同一个数组,最终爆栈。原生 flat(Infinity) 同样处理不了。要支持这种情况,得用一个 Set 记录正在展开的数组,遇到已经在里面的就跳过或报错。

时间和空间复杂度是多少?

设所有层加起来一共 n 个元素(包括子数组本身),每个元素只被访问一次,时间是 O(n)。递归版额外空间是结果数组的 O(n) 加上递归深度 d 占的调用栈 O(d);栈版把 O(d) 换成了堆上的栈数组。

从一道题,走向一组知识

把知识连起来

实用函数实现

怎么手写一个递归深拷贝,处理好对象和数组?

深拷贝和扁平化都是「遇到数组就递归」,区别是深拷贝要保留嵌套结构,扁平化要把它拆掉。

实用函数实现

手写深拷贝时,怎么用 WeakMap 解决循环引用?

数组如果引用了自己,递归扁平化会无限展开,和深拷贝一样需要一张表记录处理过的数组。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • Array.prototype.flat()

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 基础版:遇到数组就递归
  3. 对齐原生 flat:加上 depth 参数
  4. 嵌套太深时改用显式栈
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑