JavaScript合并数组的三种常用方法:concat、扩展运算符与push

作者:异步漫步时间:2026-11-05 11:22:29浏览:442

全面解析JavaScript中合并数组的三种常用方法:Array.prototype.concat、ES6扩展运算符(...)与Array.prototype.push。从不可变性、执行效率、内存占用以及浅拷贝细节等方面对比,助你根据实际场景做出最优选择。

JavaScript 数组实用指南
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
图 0:JavaScript 合并数组的核心机制概览

在日常 JavaScript 开发中,合并两个或多个数组是极高频的基础操作。最核心也是最常用的三种实现方式分别是:原生的 Array.prototype.concat() 方法、ES6 引入的 扩展运算符(...),以及原地变异的 Array.prototype.push()。这三种方式不仅在书写语法上有所区别,在底层内存分配(是否生成新数组)、运行效率、灵活性以及调用栈限制上均有明确的适用边界。

一、经典且稳健的 Array.prototype.concat()

concat() 是 ECMAScript 早期标准就具备的内置数组方法。它的最显著特征是纯函数特性(不可变性):在执行合并时,它不会修改参与合并的任何原始数组,而是开辟一块新的堆内存空间,将各数组中的元素依次复制过去,最后返回包含所有元素的新数组。

1. 基本语法与使用示例

concat() 可以接收一个或多个参数,参数既可以是数组对象,也可以是普通基础数据类型的独立值:

// 示例 1:合并两个常规数组
const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'cucumber'];

const food = fruits.concat(vegetables);

console.log(food);
// 输出示例: ['apple', 'banana', 'carrot', 'cucumber']

console.log(fruits);
// 输出示例: ['apple', 'banana'](原数组完全未受影响)

// 示例 2:合并多个数组,同时混入普通非数组元素
const numbers = [1, 2];
const combined = numbers.concat([3, 4], 5, [6, 7]);

console.log(combined);
// 输出示例: [1, 2, 3, 4, 5, 6, 7]

2. concat 的特性与适用场景

  • 不可变保障:调用者可以安全地把数据传递给外部逻辑,不用担心数组被隐式修改,非常符合函数式编程理念。
  • 参数兼容性好:即便传入的参数不是数组(如基础数值或字符串),concat() 也会自动将其作为单项元素追加,无需预先做类型包装。
  • 无调用栈溢出风险:由于其底层是由引擎原生 C++ 层面遍历复制,并非通过函数形参列表传递,因此即使传入几万甚至几十万项的大数组,也不会产生函数调用栈溢出的错误。
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
图 1:不可变模式(新建堆地址)与原地变异模式的内存结构机制对比

二、现代推荐:ES6 扩展运算符(Spread Operator)

随着 ECMAScript 2015 (ES6) 的普及,使用三个点构成的扩展运算符(Spread Syntax,...)配合数组字面量 [] 已经成为前端现代代码库中最主流的合并方案。

1. 基本语法与书写自由度

扩展运算符的作用是把一个可迭代对象(如数组)在语法层面“解构展开”为逗号分隔的参数序列。将其包裹在方括号内,就能直接构建出全新的数组字面量:

// 示例 1:使用扩展运算符合并两个数组
const primaryColors = ['red', 'green', 'blue'];
const secondaryColors = ['cyan', 'magenta', 'yellow'];

const palette = [...primaryColors, ...secondaryColors];

console.log(palette);
// 输出示例: ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow']

// 示例 2:在任意位置插入自定义元素或条件拼接
const prefix = ['start'];
const suffix = ['end'];

const workflow = ['pre', ...prefix, 'processing', ...suffix, 'done'];

console.log(workflow);
// 输出示例: ['pre', 'start', 'processing', 'end', 'done']

2. 为什么现代前端普遍青睐扩展运算符?

在 React、Vue 3 或 Redux 这类推崇数据不可变性(Immutability)的框架中,扩展运算符的书写体验具有明显优势:

  • 视觉直观明了:一目了然地看到最终数组的排布顺序,并允许在任意索引位置随心插入静态项目。
  • 高度契合现代规范:声明式代码风格比方法调用更符合当前 JavaScript 的主流审美。
  • 引擎优化充分:现代 JavaScript 引擎(如 Google V8、JavaScriptCore)针对常规大小的数组字面量扩展进行了深度的字节码层级优化,在常规业务场景下效率与 concat() 不相上下。
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
图 2:三种合并方式从变量准备到结果接收的操作时序流

三、原地变异与高效节省:Array.prototype.push()

前两种方法(concat 与扩展运算符)的核心机制都是创建并返回一个全新的数组。这意味着系统必须申请一块新的内存空间,并引发潜在的垃圾回收(GC)消耗。而在某些需要修改当前数组引用、或者对内存占用与执行频率极其敏感的高性能场景下,原地的 push() 方法则是绝佳的工具。

1. 结合扩展运算符的实现方式

默认情况下,如果直接调用 arr1.push(arr2),会将 arr2 作为一个嵌套数组放入末尾,生成二维数组 [1, 2, [3, 4]]。为了将项逐个展平合并,我们通常将 push 与扩展运算符结合使用:

// 示例:原地将 listB 的所有项追加进 listA
const targetList = [10, 20];
const incomingList = [30, 40, 50];

// push 返回的是添加新元素后数组的最终 length(此处为 5)
const newLength = targetList.push(...incomingList);

console.log(newLength);
// 输出示例: 5

console.log(targetList);
// 输出示例: [10, 20, 30, 40, 50](targetList 的原有数组已被原地修改)

console.log(incomingList);
// 输出示例: [30, 40, 50](被追加的数组完好无损)
遗留代码兼容写法:Function.prototype.apply
在不支持 ES6 扩展语法的传统环境(如古老浏览器或低版本运行时)中,开发者常用 Array.prototype.push.apply(targetList, incomingList)。其本质与 targetList.push(...incomingList) 完全一致,都是通过将待合并数组解构成实参列表传给 push 方法。

2. 致命陷阱:调用栈大小超限(Maximum call stack size exceeded)

必须格外警惕:targetList.push(...incomingList) 的原理是将 incomingList 中的每一项都解构为 push() 函数调用时的一个实参。

在所有基于 JavaScript 的运行时引擎中,单次函数调用的参数个数存在物理栈上限(V8 引擎和浏览器环境通常在 65,536 项到 100,000 项之间)。一旦待合并数组长度超过该上限,执行此代码会直接抛出致命错误:RangeError: Maximum call stack size exceeded。

// 危险示例:待合并数组规模过大
const largeBatch = new Array(100000).fill(1);
const container = [];

// 运行时极可能抛出异常:
// Uncaught RangeError: Maximum call stack size exceeded
// container.push(...largeBatch); 

// 正确方案:对于海量数据,使用循环或分批处理规避参数展开
for (const item of largeBatch) {
  container.push(item);
}
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
图 3:针对不同数据规模与不可变性要求的决策排查树

四、关键细节:浅拷贝(Shallow Copy)与对象引用陷阱

无论是 concat()、扩展运算符 [...] 还是 push(...items),它们在底层执行的全部都是浅拷贝(Shallow Copy)。

对于数值、字符串、布尔值等基础原始类型,合并时会完整复制其值,后续修改不会互相干扰;但对于包含在数组中的对象、字典、嵌套子数组等引用类型,这三种方法都只复制了指向堆内存中该对象的引用地址(指针)。

// 浅拷贝现象实录
const userA = [{ id: 101, name: 'Alice' }];
const userB = [{ id: 102, name: 'Bob' }];

// 使用扩展运算符(或 concat)进行合并
const allUsers = [...userA, ...userB];

// 尝试修改合并后新数组中的第一个用户对象的属性
allUsers[0].name = 'Alice (Updated)';

// 检查原数组 userA
console.log(userA[0].name);
// 输出示例: 'Alice (Updated)' 
// 警示:原始数组中的对象属性也被改变了!
避坑指引:如果在合并数组之后,业务逻辑需要独立修改内部对象字段且不能影响原始数据,必须对深层对象做深度克隆(例如使用现代标准的 structuredClone(sourceArray) 或第三方深拷贝工具)。

五、全维度对比与选型指南

为了让不同场景下的技术决策更加清晰,下表整理了三种合并方式的核心指标:

考量维度 Array.prototype.concat() 扩展运算符 [...a, ...b] Array.prototype.push()
对原数组影响 原数组不变(纯函数) 原数组不变(纯函数) 原地修改目标数组
调用返回值 合并后的全新 Array 合并后的全新 Array 追加后目标数组的 length
语法灵活性 仅可在末尾平铺合并 极高,可在前后或中间任意插入 仅能在尾部追加
堆内存开销 需申请新数组堆空间 需申请新数组堆空间 极低,仅对原数组扩容
超大数据量稳定性 稳定,无栈溢出问题 常规安全(字面量底层优化) 高危,项数 > 6.5 万报 RangeError
适用代表场景 遗留项目兼容、链式处理管道 React/Vue 状态更新、通用代码 高频数据缓冲追加、极客性能优化
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
图 4:全维度综合对比与技术选型决策矩阵

六、快速总结与选型法则

在绝大多数日常前端与 Node.js 业务开发中,可以依照以下三条简明的原则进行选择:

  1. 常规业务首选扩展运算符:如果只是合并两三个常规体量的数组,或者在 React/Vue 状态更新时需要返回新数组,写 [...arrA, ...arrB] 具备最好的可读性与灵活性。
  2. 追求性能与复用原容器时选 push:如果你在循环或事件回调中持续追加数据,并且明确希望直接复用现有数组容器、避免频繁 GC 垃圾回收,请使用 arrA.push(...arrB)。但切记当单次追加量可能超过数万级别时,务必改用 for...of 单项推入。
  3. 编写工具库底层兼容逻辑时选 concat:如果你需要支持更广泛的环境版本,或者希望接受不确定类型的参数(数组或单值混杂输入),arrA.concat(...) 提供了最经受时间考验的稳健表现。
作者最新文章
相关文章
更多