JavaScript数组合并:concat、扩展运算符与push的区别

作者:小括号时间:2026-11-12 09:13:44浏览:525

深入对比JavaScript中合并数组的三种主流方式:concat、扩展运算符与push。全面解析不可变性、执行效率、内存占用、超大数组调用栈限制及浅拷贝细节,并提供清晰的开发选型建议。

JavaScript数组合并:concat、扩展运算符与push的区别
图 0:JavaScript 数组合并三大主流方式(缩略总览)
在 JavaScript 开发中,合并两个或多个数组是极高频的操作。核心答案在于区分不可变操作(创建新数组引用)与原地修改(直接变更现有数组):Array.prototype.concat 与 ES6 扩展运算符 [...a, ...b] 均返回一个全新数组,原数组保持不变;而 push 搭配扩展运算符或 apply 则是原地修改目标数组,不产生新的数组分配。本文将从执行机制、内存开销、浅拷贝特性、超大数组调用栈限制及业务选型五个维度展开深入辨析。

一、三种合并方式的基础用法与语法表现

为了直观对比差异,我们先定义两个测试基础数组:const arr1 = [1, 2]; 与 const arr2 = [3, 4];。

1. Array.prototype.concat()

concat() 是 ECMAScript 早期的标准数组方法。它将传入的一个或多个参数与调用该方法的源数组拼接,并返回一个合并后的新数组。

// 示例代码:使用 concat 合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];

const result = arr1.concat(arr2);

console.log("合并结果:", result);
console.log("原数组 arr1:", arr1);
console.log("原数组 arr2:", arr2);
console.log("是否生成新引用:", result !== arr1 && result !== arr2);
示例运行输出
合并结果: [ 1, 2, 3, 4 ]
原数组 arr1: [ 1, 2 ]
原数组 arr2: [ 3, 4 ]
是否生成新引用: true

concat() 的特点在于对入参类型非常宽容。如果传入的是非数组项(例如字符串或数字),它会自动将其作为单个元素追加;如果传入的是一维数组,则会自动扁平展开该数组项。

2. ES6 扩展运算符(Spread Operator: [...])

ES6 引入的扩展运算符 ... 提供了更声明式、更灵活的语法。通过在新数组字面量中解构现有可迭代对象,实现数组拼接。

// 示例代码:使用扩展运算符合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];

// 可自由在开头、中间或末尾插入任意元素
const result = [0, ...arr1, "mid", ...arr2, 5];

console.log("合并结果:", result);
console.log("原数组 arr1:", arr1);
示例运行输出
合并结果: [ 0, 1, 2, 'mid', 3, 4, 5 ]
原数组 arr1: [ 1, 2 ]

扩展运算符不仅代码紧凑,而且适用范围更广:任何实现了 [Symbol.iterator] 协议的对象(如 Set、Map.keys()、NodeList、函数的 arguments)都可以直接展开合入数组。

3. Array.prototype.push() 搭配展开

如果直接执行 arr1.push(arr2),得到的将是嵌套数组 [1, 2, [3, 4]]。要实现合并,必须通过扩展运算符将 arr2 打散为多参数传入,或者在旧版环境中使用 Array.prototype.push.apply(arr1, arr2)。

// 示例代码:使用 push 进行原地合并
const arr1 = [1, 2];
const arr2 = [3, 4];

// push 返回的是变更后数组的新长度 (length)
const newLength = arr1.push(...arr2);

console.log("push 返回值:", newLength);
console.log("原数组 arr1 已变更:", arr1);
console.log("原数组 arr2 保持未变:", arr2);
示例运行输出
push 返回值: 4
原数组 arr1 已变更: [ 1, 2, 3, 4 ]
原数组 arr2 保持未变: [ 3, 4 ]

二、核心区别一:不可变性(Immutability)与内存引用模型

在现代 JavaScript 框架(如 React、Vue 3、Redux)中,数据驱动视图更新依赖“浅引用对比”(如 prevProps !== nextProps)。如果在组件状态中直接修改原数组,组件将无法检测到内部引用变化,从而导致界面不重新渲染。

  • 不可变合并(concat、扩展运算符):在堆内存(Heap)中重新开辟一段连续空间,将各源数组的元素拷贝过去,分配全新的内存地址并赋给新变量。源数组引用不变,零副作用。
  • 原地修改(push):直接在 arr1 已分配的堆内存块中追加元素,若现有容量不足则由 V8 引擎底层对底层数组缓冲进行扩容扩充。变量 arr1 指向的堆地址始终不变。
JavaScript数组合并:concat、扩展运算符与push的区别
图 1:不可变合并 (concat / 扩展运算符) 与 原地修改 (push) 的内存引用模型对比
框架开发注意事项
在 React 状态更新函数中,切勿写出 state.push(...newData); setState(state);。因为引用相同,React 会判定状态未发生改变而跳过更新。应当严格使用 setState([...state, ...newData])。

三、核心区别二:浅拷贝陷阱(Shallow Copy)

无论是 concat、扩展运算符还是 push,它们对数组内包含的引用类型(如 Object、Array)都只执行浅拷贝。

如果数组中存储的是引用对象,合并操作只会复制对象的指针地址,新数组与原数组中的对象依然共享同一块内存。

// 示例代码:验证合并中的浅拷贝行为
const itemObj = { id: 1, title: "原始标题" };
const listA = [itemObj];
const listB = [{ id: 2, title: "次级标题" }];

// 使用扩展运算符生成新数组
const combined = [...listA, ...listB];

// 修改新数组中对象的内部属性
combined[0].title = "篡改后的标题";

// 观察原数组 listA 中的对象属性
console.log("原 listA[0].title:", listA[0].title);
console.log("两对象引用是否相同:", combined[0] === listA[0]);
示例运行输出
原 listA[0].title: 篡改后的标题
两对象引用是否相同: true

若需要完全解除嵌套数据间的关联,必须在合并前或合并后结合深拷贝方案(如 structuredClone()),不可误认为生成了新数组就等于切断了对象内层属性的关联。

四、操作步骤与参数类型处理差异

在实际业务逻辑中,待合入的数据源并不总是规整的 Array 实例。当面对非数组值(如数字、字符串)或类数组/集合(如 Set、Map)时,三种方法有着截然不同的流程和容错行为:

JavaScript数组合并:concat、扩展运算符与push的区别
图 2:三种数组合并方法在面对不同参数类型时的执行与分流流程
  1. 非可迭代原始类型(如数字、布尔值): arr.concat(100) 会合法执行,得到 [...arr, 100];而 [...arr, ...100] 或 arr.push(...100) 则会因为数字不具备迭代器而抛出 TypeError: 100 is not iterable。
  2. Set、Map 等 ES6 集合对象: arr.concat(new Set([3, 4])) 不会展开 Set,而是把整个 Set 实例作为一个元素存入数组,结果为 [1, 2, Set(2)];而 [...arr, ...new Set([3, 4])] 会遵循迭代器协议,将 Set 中的 3 和 4 扁平展开并合入。
  3. Symbol.isConcatSpreadable 控制: concat 的扁平化行为受 ECMAScript 规范中的 Symbol.isConcatSpreadable 属性控制。普通对象如果设置了该属性为 true 并拥有 length,也可被 concat 展开;而扩展运算符只认 Symbol.iterator。

五、核心区别三:超大数组与调用栈溢出(RangeError)

许多开发者在讨论性能时常常形成一种误区:“push(...b) 是原地操作,省去了分配新数组的内存开销,所以在任何需要高性能合并的场景下都应该无脑使用 push(...b)”。

这种说法在数据量适中(数千项以内)时确实成立,但一旦进入大数据量场景(数万至数十万项),会导致严重运行时崩溃。

JavaScript数组合并:concat、扩展运算符与push的区别
图 3:超大数组使用扩展运算符传参引发调用栈溢出的原理与分块安全方案

1. 为什么 push(...b) 会报错?

当执行 arr1.push(...arr2) 时,JavaScript 引擎在编译执行时必须将 arr2 中的每一个元素转化为调用 push 方法时的单独实参。例如,长度为 100,000 的数组会被展开为 arr1.push(e0, e1, e2, ..., e99999)。

在 JavaScript 引擎(如 V8)中,单个函数调用帧(Call Stack Frame)所能压入的实参数量是有物理上限的(通常在 65,536 到 120,000 个之间,取决于宿主环境与可用栈深度)。一旦参数数量越界,引擎便会立即抛出不可恢复的运行时异常:

典型错误信息
RangeError: Maximum call stack size exceeded

2. concat 为什么能支持超大数组?

对比之下,arr1.concat(arr2) 传递的是 arr2 本身的单一对象引用(只占用 1 个参数槽位)。底层拼接是在 C++ 原生层面通过内存缓冲区直接复制元素,并不经过 JS 函数调用栈的参数展开。因此,只要系统堆内存(V8 Heap Limit)充足,concat 可以轻松合并几十万甚至数百万个元素而绝不会触发栈溢出。

3. 如果必须原地合并超大数组,应该如何解决?

如果既要节省重复创建新数组的内存占用,又需要处理超大海量数据,有两种标准的工业级安全方案:

方案 A:使用分块追加(Chunking)

// 工业级安全方案:限制单次展开规模的分块追加
function safeConcatInPlace(target, source, chunkSize = 30000) {
  for (let i = 0; i < source.length; i += chunkSize) {
    // 每次仅提取 chunkSize 个元素展开,绝不突破栈上限
    target.push(...source.slice(i, i + chunkSize));
  }
  return target;
}

const target = [1, 2];
const hugeArray = new Array(200000).fill(9);

safeConcatInPlace(target, hugeArray);
console.log("安全合并完成,新长度:", target.length);
示例运行输出
安全合并完成,新长度: 200002

方案 B:简单的 for...of 循环

直接使用循环逐个追加虽然看似原始,但它零开销、无栈深限制、无需额外创建切片,在处理巨量数据时在 V8 引擎中具有非常可观的 JIT 优化速度。

六、综合对比与选型指南

为了在日常编码中快速作出准确判断,下表和图表从语言特性、执行特征与适用边界对三种方式进行全方位总结:

JavaScript数组合并:concat、扩展运算符与push的区别
图 4:concat、扩展运算符与 push 综合对比矩阵
特性维度 Array.concat() 扩展运算符 [...] arr.push(...items)
原数组状态 不可变(保持原样) 不可变(保持原样) 原地修改(追加变更)
返回值 全新数组实例 全新数组实例 操作后数组的新 length
代码灵活性 方法调用,按序拼接 最高,可任意穿插单元素 向后追加,前置插入需用 unshift
对非数组兼容 自动当做单元素合入 非可迭代对象抛 TypeError 非可迭代对象抛 TypeError
迭代器(Set/Map)支持 不展开,视作整体对象 原生展开集合内元素 原生展开集合内元素
内存开销 重新分配整段内存,触发GC 重新分配整段内存,触发GC 极低,重用已有内存空间
超大数组安全 极安全(底层原生复制) 超十万项存在栈溢出风险 超十万项引发栈溢出崩溃

七、常见场景选型规则

在实际项目编码中,可以遵循以下三条判定准则选择最合适的合并方案:

  1. 日常业务逻辑与组件状态管理(默认首选):优先使用 ES6 扩展运算符 [...a, ...b]。它的语法最直观,符合函数式编程无副作用理念,完美契合 React、Vue 3 等现代响应式体系。
  2. 性能敏感型算法、大量循环数据累加:优先使用 push 原地修改。例如在批量拉取分页数据并合并进本地缓存缓冲池时,原地修改避免了每页都分配一个逐渐庞大的临时新数组,显著减少了垃圾回收器(GC)的停顿开销。
  3. 涉及不可控的大规模数据(> 50,000 项)且要求不可变:选择 concat(),规避函数实参压栈上限造成的 RangeError。若必须原地修改超大数组,则使用分块批处理(Chunking)。
作者最新文章
相关文章
更多