深拷贝 vs 浅拷贝:JavaScript 中对象复制的陷阱与技巧


在 JavaScript 开发中,对象的复制是一个常见但容易出错的操作。由于 JavaScript
中对象是通过引用传递的,不恰当的复制方式可能导致意想不到的副作用,比如修改复制后的对象意外地影响到原始对象。理解深拷贝和浅拷贝的区别,掌握各种复制技巧,对于编写可靠、健壮的代码至关重要。

引用类型的特性

在深入探讨拷贝方法之前,我们需要理解 JavaScript 中的基本类型和引用类型的区别:

  • 基本类型(如 number、string、boolean):按值存储和传递
  • 引用类型(如 object、array、function):按引用存储和传递

当你将一个对象赋值给另一个变量时,实际上只是复制了指向该对象的引用,而不是对象本身的内容:

const original = { name: "John" };  
const copy = original;  
  
copy.name = "Jane";  
console.log(original.name); // 输出: "Jane"  

这就是为什么我们需要不同的拷贝策略。

浅拷贝 (Shallow Copy)

浅拷贝创建一个新对象,但只复制原始对象第一层属性的值。如果属性是基本类型,则复制其值;如果属性是引用类型,则复制其引用(地址)。

浅拷贝的实现方法

  1. Object.assign()
const original = { name: "John", details: { age: 30 } };  
const shallowCopy = Object.assign({}, original);  
  
shallowCopy.name = "Jane"; // 不影响原对象  
shallowCopy.details.age = 25; // 影响原对象!  
  
console.log(original.name); // 输出: "John"  
console.log(original.details.age); // 输出: 25  
  1. 展开运算符 (Spread Operator)
const original = { name: "John", details: { age: 30 } };  
const shallowCopy = { ...original };  
  
// 行为与 Object.assign() 相同  
  1. 数组的浅拷贝方法
// 使用 slice()  
const originalArray = [1, 2, { value: 3 }];  
const slicedArray = originalArray.slice();  
  
// 使用展开运算符  
const spreadArray = [...originalArray];  
  
// 使用 Array.from()  
const fromArray = Array.from(originalArray);  
  
// 所有这些方法都只创建浅拷贝  
slicedArray[2].value = 100;  
console.log(originalArray[2].value); // 输出: 100  

浅拷贝的陷阱

浅拷贝的主要问题是:对于嵌套对象或数组,修改副本中的嵌套结构会影响原始对象。这是因为嵌套对象的引用在原始对象和副本之间是共享的。

深拷贝 (Deep Copy)

深拷贝创建一个新对象,并递归地复制原始对象的所有嵌套对象,确保副本与原始对象完全独立。

深拷贝的实现方法

  1. JSON 序列化/反序列化

最简单(但有局限)的深拷贝方法:

const original = { name: "John", details: { age: 30 } };  
const deepCopy = JSON.parse(JSON.stringify(original));  
  
deepCopy.details.age = 25;  
console.log(original.details.age); // 输出: 30,原对象不受影响  

局限性:

  • 不能复制函数、undefined、Symbol、BigInt
  • 不能处理循环引用
  • 丢失原型链
  • 不能正确处理 Date、RegExp、Map、Set 等特殊对象
  1. 递归实现深拷贝

这个简单实现可以应对大多数场景,但在实际项目中,可能需要更完善的版本来处理循环引用、特殊对象类型等情况。

  1. 使用库

在生产环境中,通常推荐使用经过充分测试的库来处理深拷贝:

  • lodash 的 _.cloneDeep()
  • rfdc (Really Fast Deep Clone)
  • structuredClone()(新的原生 API)

结构化克隆算法 (structuredClone)

structuredClone() 是一个相对较新的全局方法,它实现了结构化克隆算法,可以创建深层次的副本:

优势:

  • 原生 API,无需依赖外部库
  • 可以处理大多数 JavaScript 内置类型
  • 支持循环引用
  • 性能通常较好

局限性:

  • 不能克隆函数
  • 不能克隆 DOM 节点
  • 不会保留对象的原型链

性能考量

深拷贝通常比浅拷贝消耗更多资源,特别是对于大型、复杂的数据结构。在选择拷贝策略时,应考虑以下几点:

  1. 数据结构的大小和复杂度
  2. 性能要求
  3. 对象的使用方式(是否需要完全独立的副本)

实用技巧

1. 混合拷贝策略

有时,你可能只需要对特定的嵌套属性进行深拷贝:

2. 不可变数据模式

采用不可变数据模式,而不是直接修改对象:

3. 使用 Object.freeze() 防止修改

注意: Object.freeze() 只冻结对象的第一层属性。

常见陷阱与解决方案

陷阱 1:意外的副作用

解决方案:使用深拷贝或者明确地复制需要修改的嵌套结构。

陷阱 2:过度深拷贝

解决方案:只在必要时使用深拷贝,或者只深拷贝需要修改的部分。

陷阱 3:特殊对象类型

const original = {  
date: newDate(),  
regex: /pattern/,  
func: function() { returntrue; }  
};  
  
// JSON 方法会丢失或错误转换这些特殊类型  
const copy = JSON.parse(JSON.stringify(original));  
console.log(copy.date); // 字符串,而非 Date 对象  
console.log(copy.regex); // 空对象 {}  
console.log(copy.func); // undefined  

解决方案:使用专门的深拷贝库或自定义函数来处理特殊类型。

最佳实践

  1. 明确需求:首先确定你是否真的需要深拷贝。很多时候,浅拷贝或部分深拷贝就足够了。
  2. 选择合适的工具:
    • 浅拷贝: Object.assign() 或展开运算符
    • 简单深拷贝: structuredClone() JSON.parse(JSON.stringify())
    • 复杂深拷贝:lodash 的 _.cloneDeep() 或自定义递归函数
  3. 测试边缘情况:特别是当处理包含特殊对象类型或循环引用的数据时。
  4. 考虑不可变数据模式:使用不可变数据模式可以减少对深拷贝的需求。
  5. 性能平衡:在深拷贝和性能之间找到平衡点,尤其是在处理大型数据结构时。