1. 背景与核心问题
数组复制的目标
在 JavaScript 中,复制一个数组通常意味着创建一个新数组,其元素与原数组在位置上对应,且对引用类型的元素只是拷贝引用。这是一个典型的浅拷贝过程,与对数组中对象、数组等引用值的深拷贝不同。
本文围绕两种主流的实现方式展开分析:…arr(展开语法)与 new Array(...arr),并比较它们在语义、处理空洞、以及对可迭代对象的支持方面的差异。
通过对比,可以把握在不同场景下的行为规律,例如稀疏数组的空洞与一般数组元素复制时的差异。关键点包括空洞的保留、长度的一致性、以及对引用值的浅拷贝特性。

两种方法的基本差异
展开运算符(…arr)直接把原数组的元素展开为新数组的元素,因此<结果的长度与原数组相同,并且空洞通常会被保留为稀疏结构。
而通过 new Array(...arr) 传入展开结果作为构造函数参数时,如果 arr 含有空洞,构造出的新数组会把它们转化为 undefined,从而不再是纯粹的空洞。
另一方面,对于迭代对象,两者都可以工作,但实现细节略有不同,需结合具体环境的引擎实现来理解性能特征。
2. 展开运算符…arr 的工作原理
语义要点
展开运算符将可枚举的自有属性逐一展开成独立的数组元素。它属于数组字面量和函数调用中的简写语法,便于直接复制或组合数组。
在实现层面,…arr 会读取原数组的长度、逐个读取下标并传入新数组,从而实现“逐项拷贝”的效果。
对于普通、完整的数组,这种拷贝通常是快速且直观的。这是最常见的实现方式之一,在代码可读性与简洁性方面有显著优势。
对空洞的处理
对于包含空洞的数组,展开运算符会保留空洞,新数组在相同的位置处仍是空洞,不会插入 undefined 值。
这意味着在遍历或映射时,某些方法会略有不同,例如 map 会跳过空洞区域,不对空洞进行回调。保持空洞语义与原始数组一致性很重要。
下面给出一个简短例子,帮助理解这一行为:空洞 vs undefined在输出中的表现差异。
const a = [1, , 3];
const b = [...a];
console.log(b); // [1, <1 empty item>, 3]
console.log(0 in b); // true
console.log(1 in b); // false
3. new Array(...arr) 的行为与边界
参数机制与结果
使用 new Array(...arr) 时,展开后的元素通过作为构造函数的多个实参传入。这会创建一个新数组,其长度和内容取决于传入的参数序列,而不是简单地得到一个长度一致的新副本。
如果传入的参数很多,新数组的长度等于参数个数,包含对应的元素值;如果 arr 是一个数字且作为唯一参数传入,则会得到一个指定长度的空数组。
与直接拷贝相比,参数分解的方式对空洞处理有显著不同,会把空洞转化为显式的 undefined 元素。
空洞与 undefined 的转化
对于包含空洞的 arr,新数组中的对应位置通常是 undefined,因为空洞在参数传入阶段被视作未传入参数,设计上会变成未定义的实参。
这与展开运算符的行为形成对比:展开运算符保留空洞,而 new Array(...arr) 会将其转化为 undefined,从而影响后续的遍历和条件判断。
const a = [1, , 3];
const e = new Array(...a);
console.log(e); // [1, undefined, 3]
console.log(2 in e); // true
console.log(1 in e); // true, because it's defined with undefined value
对可迭代对象的扩展
当 arr 是一个可迭代对象(如字符串、Set、Generator 等)时,两种方式都会将其展开为一个普通数组,但产生的结果在对空洞的处理上并不涉及空洞的概念,因为迭代对象本身不具备“空洞”的语义。
在这类场景中,展开运算符的直观性更高,因为它直接将迭代的元素按序列拼接到新数组中。
4. 对比要点与最佳实践
对比要点速览
总览两种方法在核心行为上的差异:展开运算符更倾向于保留空洞和原始长度,而 new Array(...arr) 会把空洞转换为 undefined,同时将展开结果作为构造函数的参数。
在处理可迭代对象时,两者都可以得到一个新的数组,但
一个关键点是引用类型元素的拷贝:无论哪种方法,都是浅拷贝(元素的引用被复制),而非深拷贝。
// 示例对比
const arr = [{a:1}, {b:2}, 3];
const withSpread = [...arr];
const withNewArray = new Array(...arr);
console.log(withSpread[0] === withNewArray[0]); // true (同一对象引用)
性能与可读性考虑
在多数引擎中,展开运算符通常具有更好的性能与优化路径,因为它是语言级别的表达式,编译时可优化。
从可读性角度看,…arr 的意图更明确:复制原数组的结构和内容,这也是业界常用的写法之一。
另一方面,new Array(...arr) 的语义稍微复杂一些,且对空洞的处理会带来不一致的遍历结果,因此在混合了稀疏数组的场景中需要额外留意。
实际开发中的常见陷阱
出现错误的常见原因包括:对空洞的处理期望与实际结果不一致、将稀疏数组误以为是完全定长的数组、以及将单一数字作为 new Array 的参数造成长度变化。
在涉及多维数组或嵌套结构的赋值时,浅拷贝的局限性尤为突出,需要考虑是否需要深拷贝或自定义拷贝方法。


