JS合并多个数组的三种方法:concat、spread与flat
深入解析 JavaScript 合并多个数组的三种主流方法:concat()、扩展运算符(...)与 flat()。详细讲解各方法的语法使用、浅拷贝机制、多维嵌套处理、稀疏数组特性及技术选型建议。
在日常 JavaScript 开发中,将分散在多个数组中的数据合并为一个整体是非常普遍的高频操作。无论是组合分页请求返回的数据、聚合来自不同数据源的列表,还是把嵌套结构打平成一维结构,JavaScript 都提供了灵活且成熟的解决方案。
本文聚焦于三种最常见且实用的合并方式:Array.prototype.concat()、ES6 的扩展运算符(Spread Syntax ...),以及 ES2019 引入的 Array.prototype.flat()。它们的核心思路是什么?语法如何编写?在对象浅拷贝、嵌套数组、稀疏数组与运行时性能上有何差异?读完本文,你将能够根据具体业务场景,迅速选择最适宜、代码表现力最强的实现方案。
一、方法一:Array.prototype.concat() 传统而稳健的数组连接
concat() 是 ECMAScript 早期标准中就已具备的原型方法。它的工作方式十分明确:以调用该方法的宿主数组为基础,按参数顺序将后续传入的数组或单个值拼接到尾部,最后返回一个全新创建的数组实例。整个过程严格保持“不可变性”(Immutability),绝不会修改任何参与合并的原数组。
1. 基础用法与多参数连接
concat() 可以同时接收任意多个参数。每个参数既可以是一个数组,也可以是单个基础类型数值或对象:
// 声明三个基础数据数组
const listA = ['苹果', '香蕉'];
const listB = ['橙子', '葡萄'];
const listC = ['西瓜'];
// 合并多个数组,并同时混入单项元素
const mergedFruits = listA.concat(listB, listC, '草莓');
console.log(mergedFruits);
// 输出: ['苹果', '香蕉', '橙子', '葡萄', '西瓜', '草莓']
// 验证不可变性:原数组保持完好
console.log(listA); // ['苹果', '香蕉']
console.log(listA === mergedFruits); // false (完全独立的新引用)
2. 浅拷贝(Shallow Copy)的核心机制与注意事项
初学者容易产生一个误解,认为 concat() 既然返回了新数组,那么新数组里的数据就和旧数组彻底“脱钩”了。实际上,concat() 执行的是浅拷贝:
- 对于数字、布尔值、字符串等原始类型,其数值会被直接复制,新旧互不影响;
- 对于对象、数组、函数等引用类型,新数组中存放的仅仅是指向原有堆内存地址的对象指针。一旦通过新数组修改了对象的属性,原数组中对应对象的状态也会同步发生改变。
const sourceArr = [{ id: 1, name: 'Alice' }];
const copyArr = sourceArr.concat();
// 修改新数组中首个对象的属性
copyArr[0].name = 'Bob';
// 原数组中的对象同样被改变,因为共享同一内存引用
console.log(sourceArr[0].name); // 输出: 'Bob'
二、方法二:扩展运算符(Spread Syntax ...)声明式解构合并
随着 ECMAScript 2015 (ES6) 的正式落地,扩展运算符(...)成为了现代 JavaScript 中最主流、最优雅的数组操作方式。它通过解构语法,直接在新的数组字面量 [ ... ] 中将源数组“就地摊平”,不仅可读性极佳,还能带来极高的组合自由度。
1. 语法直观与任意位置灵活插值
与 concat() 相比,扩展运算符最大的优势在于它允许在数组字面量的任意位置自由插拔数据或嵌入条件表达式:
const basicColors = ['红', '黄'];
const coolColors = ['蓝', '绿'];
const hasNeutral = true;
// 在开头、中间、末尾任意插值或结合三元表达式
const palette = [
'黑',
...basicColors,
...(hasNeutral ? ['灰'] : []),
...coolColors,
'白'
];
console.log(palette);
// 输出: ['黑', '红', '黄', '灰', '蓝', '绿', '白']
2. 原生支持一切可迭代对象(Iterable)
concat() 如果直接接收一个 Set 或 DOM NodeList 参数,会默认将其视作单一元素整体压入,而不会自动展开集合。但扩展运算符遵循标准的迭代器协议(Symbol.iterator),能无缝解包任何可迭代对象:
const arrayA = [1, 2, 3];
const uniqueSet = new Set([3, 4, 5]);
// 直接利用展开语法解构 Set 并完成合并
const merged = [...arrayA, ...uniqueSet];
console.log(merged); // [1, 2, 3, 3, 4, 5]
// 若需要合并后保持唯一性,可再次套用 Set
const deduped = [...new Set([...arrayA, ...uniqueSet])];
console.log(deduped); // [1, 2, 3, 4, 5]
如果使用 arr1.push(...arr2) 将后一个数组原地合并到前一个数组中,当 arr2 的元素数量达到几十万级别时,展开后的实参列表可能会超出 JavaScript 引擎的单次函数调用栈限制(抛出 RangeError: Maximum call stack size exceeded)。但在字面量 [...arr1, ...arr2] 声明中构建新数组不会受调用栈限制影响,仅受可用堆内存制约。
三、方法三:Array.prototype.flat() 扁平化多维合并与空位清洗
前面介绍的 concat() 和扩展运算符主要适用于处理扁平的一维数组。然而在现实业务场景中,我们常常会遇到更复杂的数据结构:例如后端 API 返回了一个“数组的数组”([[1, 2], [3, 4]]),或者子项中存在未知深度的嵌套子数组。如果此时依然用 concat 或 ...,只能剥离最外层一层,内层数组依然原样保留。
ES2019 正式引入了 Array.prototype.flat() 方法,专门用于递归打平多维数组,并为数组合并提供了全新的思路。
1. 外层数组包裹与深度控制
我们可以先将多个目标数组用一个外层数组统一包裹([arr1, arr2, arr3]),再调用 flat() 一键将其合并为一维平面:
const arr1 = [1, 2];
const arr2 = [3, [4, 5]];
const arr3 = [6, [7, [8, 9]]];
// 默认 flat(1):仅展开最外层包裹
const flatOne = [arr1, arr2, arr3].flat();
console.log(flatOne);
// 输出: [1, 2, 3, [4, 5], 6, [7, [8, 9]]]
// 显式指定深度 depth = 2
const flatTwo = [arr1, arr2, arr3].flat(2);
console.log(flatTwo);
// 输出: [1, 2, 3, 4, 5, 6, 7, [8, 9]]
// 传入 Infinity:无论嵌套多少层,彻底展开为纯一维数组
const flatAll = [arr1, arr2, arr3].flat(Infinity);
console.log(flatAll);
// 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
2. 独特的稀疏数组(Sparse Array)空洞清洗特性
在数组处理中,因跳跃赋值或使用 new Array(n) 创建的数组往往会包含空槽(empty slots)。令人惊喜的是,flat() 在执行降维时会自动跳过并剔除这些空位,而 concat() 和展开语法则不会清除这些空位:
// 构造包含稀疏空槽的数组
const sparseArr = [1, , 2]; // 索引 1 处为 empty
const normalArr = [3, 4];
// 使用 concat 合并:空槽依然存在
console.log(sparseArr.concat(normalArr));
// [1, empty, 2, 3, 4]
// 使用 flat 合并:空槽被自动清洗丢弃
console.log([sparseArr, normalArr].flat());
// [1, 2, 3, 4]
四、三种方法的特性横向对比与技术选型指南
为了帮助大家在日常编码中快速做决策,我们将这三种数组合并方法在语言版本、不可变性、嵌套处理、稀疏空槽表现及适用场景等方面进行了全方位对比:
| 对比维度 | Array.prototype.concat() | 扩展运算符(...) | Array.prototype.flat() |
|---|---|---|---|
| 语法类别 | Array 原型链内置方法 | ES6 声明式解构语法 | ES2019 原型链内置方法 |
| 是否修改原数组 | 否(返回新实例) | 否(返回新实例) | 否(返回新实例) |
| 拷贝深度机制 | 浅拷贝(共享对象指针) | 浅拷贝(共享对象指针) | 浅拷贝(共享对象指针) |
| 多维嵌套支持 | 仅展开实参第一层 | 仅展开第一层 | 支持自定义深度及 Infinity 彻底打平 |
| 稀疏数组空槽 | 保留 empty 槽位 |
空位解构为 undefined |
自动过滤并移除空位 |
| 类数组/Set 支持 | 作为单项整体追加(需先转数组) | 原生无缝解构所有 Iterable | 需先在外层完成包裹 |
| 首选适用场景 | 老旧系统兼容、函数链式调用 | 现代常规开发、插值与条件合并 | 动态数组集合、深层嵌套打平、数据清洗 |
深拷贝痛点的终极解决方案
正如前文分析,以上三种合并方法无一例外都是浅拷贝。如果你的数组内部包含复杂嵌套对象,并且业务严格要求合并后的操作绝不能影响源数据,请务必在合并后执行深拷贝,或提前对源数据进行深拷贝:
// 现代浏览器及 Node.js 17+ 原生支持 structuredClone
const listA = [{ user: 'Tom' }];
const listB = [{ user: 'Jerry' }];
// 合并并完成完全隔离的深拷贝
const deeplyMerged = structuredClone([...listA, ...listB]);
deeplyMerged[0].user = 'Spike';
console.log(listA[0].user); // 输出仍为 'Tom',源数据受到绝对保护
五、总结与工程落地建议
在 JavaScript 中合并多个数组并不存在绝对的“最佳方法”,关键在于根据具体的输入形态和业务诉求进行选型:
- 常规一维数组合并首选扩展运算符(
...):现代语法标准、清晰直观、支持随处插入单个元素或条件参数,并且天然兼容Set与NodeList等可迭代对象; - 需要展开多维嵌套或清除空槽时首选
flat():把零散数组放入统一外层容器[a, b, c].flat(Infinity),不仅能够递归降维展开任意未知深度的嵌套数组,还能顺带清除稀疏空位,是处理复杂数据聚合的利器; - 兼容旧版本运行环境或偏好面向对象链式风格时使用
concat():作为老牌原生 API,它语义清晰稳定,在各端运行环境中均有高度一致的表现。
掌握这三种方法的底层机制与边界条件,不仅能让你的日常业务代码更加简洁高效,更能从容规避浅拷贝污染与栈溢出等常见隐藏陷阱。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01








