前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 热点
旧版
首页程序员面试题库concat 与展开运算符嵌套数组
JavaScript数组与集合

concat 和展开运算符合并数组有什么区别?嵌套数组和空位怎么处理?

合并两个普通数组时,a.concat(b) 和 [...a, ...b] 结果一样:都只拆开最外面一层,里面的数组和对象还是原来那一份引用。区别在于 concat 只拆数组,展开能拆 Set、字符串这类可迭代对象;concat 会保留空位,展开会把空位变成 undefined。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#数组与集合#容器化
先看核心答案读代码示例
理解线索

两种合并方式各按什么规则拆

  1. concat参数是数组(或 Symbol.isConcatSpreadable 为真)就拆一层,否则整体追加
  2. 展开语法调用 Symbol.iterator 逐个取值,不可迭代的对象直接报 TypeError
  3. 浅拷贝新数组是新的,里面的对象和子数组仍然和原数组共用

[].concat(new Set([1, 2])) 得到的是只有一个元素的数组,那个元素就是 Set 本身,不会被拆开。

核心回答

先记住这个答案

对普通数组来说,a.concat(b) 和 [...a, ...b] 都会创建一个新数组,都只拆开最外层,内层的数组和对象仍然和原数组共享引用,所以两者都是浅合并。区别出在非普通数组的输入上:concat 只拆数组,传入 Set、字符串、普通对象都整体追加;展开语法走迭代器协议,可迭代的都会被拆开,不可迭代的直接报错。另外 concat 会保留稀疏数组的空位,展开则把空位读成 undefined。

  • 两者都只拆一层,都是浅拷贝
  • concat 只展开数组参数,其它值原样追加
  • 展开按迭代器取值,Set 和字符串也会被拆开
  • concat 保留空位,展开把空位变成 undefined

普通数组:结果一样,都是浅的

先看最常见的情况,合并两个普通数组,其中有嵌套数组和对象。

嵌套只拆一层,对象共享引用JavaScript
const inner = { n: 1 }
const a = [1, [2, 3]]
const b = [inner]

const c1 = a.concat(b, 4) // 4 不是数组,直接追加
const c2 = [...a, ...b, 4]
console.log(JSON.stringify(c1), JSON.stringify(c2))

inner.n = 99 // 改原对象
console.log(c1[2].n, c2[2].n) // 两个新数组里都变了
console.log(c1[1] === a[1]) // 内层数组是同一个
查看输出与解释
[1,[2,3],{"n":1},4] [1,[2,3],{"n":1},4]
99 99
true

[2, 3] 没有被拆开,因为两种方式都只处理传进来的那一层。新数组像一个新的书架,书还是原来那几本,谁在上面做了批注,另一个书架上的也跟着变。要彻底断开引用,得用 structuredClone 或者自己递归复制。

非普通数组:拆不拆、怎么拆不一样

concat 判断的是「这个参数是不是数组」,展开判断的是「这个值能不能迭代」。标准不同,遇到边界输入结果就不同了。

空位、Set 和字符串JavaScript
const sparse = [1, , 3] // 下标 1 是空位,不是 undefined
const s = new Set([4, 5])

const x = [].concat(sparse)
const y = [...sparse]
console.log(1 in x, 1 in y) // concat 保留空位,展开变成真实的 undefined

console.log([].concat(s).length, [...s].length) // Set 整体追加 vs 拆开
console.log([].concat('ab').length, [...'ab'].length) // 字符串同理
查看输出与解释
false true
1 2
1 2

空位指数组在这个下标上根本没有属性,1 in x 是 false。而 concat 复制时会先检查属性是否存在,不存在就跳过,所以空位保留了下来。展开是通过迭代器一个个取值的,迭代器读到空位就返回 undefined,于是 y 的下标 1 上真的有了一个 undefined。这个差别会影响后面的 forEach、map,它们会跳过空位,但不会跳过 undefined。

用 Symbol.isConcatSpreadable 改变 concat 的行为
concat 其实是看参数的 Symbol.isConcatSpreadable 属性,没设置时才退回到「是不是数组」。给一个类数组对象设置这个属性为 true,那么 concat 就会按 length 和下标拆开它;给数组设置为 false,它就会被整体追加。日常业务很少用,但它解释了 concat 为什么能区别对待不同参数。
回答前,多想一步

容易答错的地方

以为 concat 会把嵌套数组全部拍平
[1].concat([[2, [3]]]) 得到的是 [1, [2, [3]]],只拆了参数本身那一层。要拍平多层用 flat(depth),比如 flat(Infinity) 拍到底。
以为展开出来的是深拷贝
[...arr] 常被用来复制数组,但复制的只是外层。在 React 里用展开复制状态数组后直接改里面的对象,原状态也被改了,组件可能不会重新渲染。
以为展开能处理任何对象
数组字面量里的展开要求值可迭代。[...{ length: 2 }] 会抛 TypeError,因为普通对象没有 Symbol.iterator。类数组对象要先用 Array.from 转换。
试着用自己的话回答

面试官还会怎么问?

往已有数组追加一大批元素,push(...big) 有什么问题?

push(...big) 会把每个元素都变成一个函数参数,数组有几十万项时可能超出引擎的参数数量上限,抛 RangeError。追加大数组可以分批 push,或者直接用 concat 生成新数组。

concat 和展开哪个更快?

不同引擎、不同数组大小结论不一样,没有固定答案。合并普通数组时两者差别通常可以忽略,按可读性选就行,真遇到性能热点再用实际数据测。

合并时想去重怎么写?

[...new Set([...a, ...b])]。先合并,再交给 Set 去重,最后展开回数组。Set 判断相等用的是 SameValueZero,NaN 能被正确去重,但两个内容相同的对象会被当成不同元素。

从一道题,走向一组知识

把知识连起来

数组与集合

copyWithin 是怎么在数组内部复制的?源和目标重叠时会覆盖错吗?

concat 返回新数组,copyWithin 在原数组内部搬数据,一个不改原数组一个改。

数组与集合

数组展开为什么能复制数组和 Set,它背后到底读取了什么?

展开靠的是迭代器协议,想知道哪些对象能被展开、为什么,接着看这题。

数组与集合

flat 默认展开几层?flatMap 和先 map 再 flat 有什么不同?

两种合并都只拆一层,要拆多层就得用 flat 并指定深度。

参考资料

  • MDN:concat
  • MDN:展开语法

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

本题目录
  1. 先记住这个答案
  2. 普通数组:结果一样,都是浅的
  3. 非普通数组:拆不拆、怎么拆不一样
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

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

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