在 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)
浅拷贝创建一个新对象,但只复制原始对象第一层属性的值。如果属性是基本类型,则复制其值;如果属性是引用类型,则复制其引用(地址)。
浅拷贝的实现方法
- 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
- 展开运算符 (Spread Operator)
const original = { name: "John", details: { age: 30 } };
const shallowCopy = { ...original };
// 行为与 Object.assign() 相同
- 数组的浅拷贝方法
// 使用 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)
深拷贝创建一个新对象,并递归地复制原始对象的所有嵌套对象,确保副本与原始对象完全独立。
深拷贝的实现方法
- 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 等特殊对象
- 递归实现深拷贝

这个简单实现可以应对大多数场景,但在实际项目中,可能需要更完善的版本来处理循环引用、特殊对象类型等情况。
- 使用库
在生产环境中,通常推荐使用经过充分测试的库来处理深拷贝:
- lodash 的
_.cloneDeep() - rfdc (Really Fast Deep Clone)
- structuredClone()(新的原生 API)

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

优势:
- 原生 API,无需依赖外部库
- 可以处理大多数 JavaScript 内置类型
- 支持循环引用
- 性能通常较好
局限性:
- 不能克隆函数
- 不能克隆 DOM 节点
- 不会保留对象的原型链
性能考量
深拷贝通常比浅拷贝消耗更多资源,特别是对于大型、复杂的数据结构。在选择拷贝策略时,应考虑以下几点:
- 数据结构的大小和复杂度
- 性能要求
- 对象的使用方式(是否需要完全独立的副本)
实用技巧
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
解决方案:使用专门的深拷贝库或自定义函数来处理特殊类型。
最佳实践
- 明确需求:首先确定你是否真的需要深拷贝。很多时候,浅拷贝或部分深拷贝就足够了。
- 选择合适的工具:
- 浅拷贝:
Object.assign()或展开运算符 - 简单深拷贝:
structuredClone()或JSON.parse(JSON.stringify()) - 复杂深拷贝:lodash 的
_.cloneDeep()或自定义递归函数
- 浅拷贝:
- 测试边缘情况:特别是当处理包含特殊对象类型或循环引用的数据时。
- 考虑不可变数据模式:使用不可变数据模式可以减少对深拷贝的需求。
- 性能平衡:在深拷贝和性能之间找到平衡点,尤其是在处理大型数据结构时。