引言:为什么我们需要关注深浅拷贝?
在日常编程中,数据复制是我们经常要处理的问题。然而,简单的赋值操作往往会导致意想不到的bug。让我们从一个常见的场景开始:
javascript
const original = {
name: "张三",
address: {
city: "北京",
district: "海淀区"
}
};
const copy = original;
copy.name = "李四";
copy.address.city = "上海";
console.log(original.name); // "李四" - 意外修改!
console.log(original.address.city); // "上海" - 意外修改!
看到问题了吗?我们只是想修改副本,却意外地改变了原始数据。这就是理解深浅拷贝至关重要的原因。在本文中,我们将深入探讨深浅拷贝的原理、实现方式和最佳实践。
一、基本概念:值类型与引用类型
1.1 JavaScript中的数据类型
要理解拷贝,首先需要明白JavaScript的数据类型分类:
值类型(基本类型):
-
String、Number、Boolean、undefined、null、Symbol、BigInt
-
存储在栈内存中
-
赋值时创建值的副本
javascript
let a = 10;
let b = a; // 创建值的副本
b = 20;
console.log(a); // 10 - 原始值不变
引用类型(对象类型):
-
Object、Array、Function、Date、RegExp等
-
存储在堆内存中
-
变量存储的是内存地址的引用
javascript
let obj1 = { name: "张三" };
let obj2 = obj1; // 复制引用(内存地址)
obj2.name = "李四";
console.log(obj1.name); // "李四" - 原始对象被修改!
1.2 赋值 vs 浅拷贝 vs 深拷贝
| 操作类型 | 描述 | 影响 |
|---|---|---|
| 赋值 | 复制引用地址 | 新旧变量指向同一对象 |
| 浅拷贝 | 创建新对象,复制一层属性 | 嵌套对象仍共享引用 |
| 深拷贝 | 完全复制,包括所有嵌套对象 | 新旧对象完全独立 |
二、浅拷贝(Shallow Copy)的实现方式
2.1 对象浅拷贝方法
方法1:扩展运算符(ES6)
javascript
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
// 修改浅层属性
shallowCopy.a = 3;
console.log(original.a); // 1 - 未改变
// 修改深层属性
shallowCopy.b.c = 4;
console.log(original.b.c); // 4 - 被改变!
方法2:Object.assign()
javascript
const original = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, original);
// 同样的问题:嵌套对象被共享
shallowCopy.b.c = 3;
console.log(original.b.c); // 3
方法3:数组的浅拷贝方法
javascript
const arr = [1, 2, { a: 3 }];
// 方法1:slice()
const copy1 = arr.slice();
// 方法2:concat()
const copy2 = [].concat(arr);
// 方法3:扩展运算符
const copy3 = [...arr];
// 方法4:Array.from()
const copy4 = Array.from(arr);
// 所有方法都有相同的问题:嵌套对象是共享的
copy1[2].a = 4;
console.log(arr[2].a); // 4
2.2 浅拷贝的手动实现
javascript
function shallowCopy(obj) {
// 处理非对象类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
return [...obj];
}
// 处理普通对象
return { ...obj };
}
// 或者更通用的实现
function shallowCopy(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
const result = Array.isArray(target) ? [] : {};
for (let key in target) {
// 只复制对象自身的属性(不包括原型链上的属性)
if (target.hasOwnProperty(key)) {
result[key] = target[key];
}
}
return result;
}
2.3 浅拷贝的使用场景
浅拷贝适用于以下情况:
-
对象结构简单,没有嵌套引用
-
明确知道需要共享某些嵌套对象
-
性能要求高,且数据量不大
javascript
// 场景:表单状态重置
const defaultForm = {
username: '',
email: '',
settings: { theme: 'light' } // 这个对象可以被共享
};
function resetForm() {
return { ...defaultForm, username: '', email: '' };
// settings对象被共享,符合需求
}
三、深拷贝(Deep Copy)的实现方式
3.1 简单的深拷贝方法及其局限性
方法1:JSON.parse(JSON.stringify())
javascript
const original = {
name: "张三",
address: { city: "北京" },
date: new Date(),
func: function() { console.log('hello'); },
undefined: undefined,
symbol: Symbol('sym'),
bigint: BigInt(123),
infinity: Infinity,
nan: NaN,
regex: /pattern/gi
};
const deepCopy = JSON.parse(JSON.stringify(original));
console.log(deepCopy);
// 输出:
// {
// name: "张三",
// address: { city: "北京" },
// date: "2023-10-01T00:00:00.000Z", // 转为字符串
// infinity: null, // Infinity被转为null
// nan: null, // NaN被转为null
// regex: {} // 空对象
// }
// 丢失了:func, undefined, symbol, bigint
JSON方法的局限性:
-
函数、undefined、Symbol被忽略
-
Date对象转为字符串
-
RegExp对象转为空对象
-
Infinity、NaN被转为null
-
无法处理循环引用
-
无法复制原型链
方法2:structuredClone(现代浏览器API)
javascript
const original = {
name: "张三",
address: { city: "北京" },
date: new Date(),
array: [1, 2, 3],
set: new Set([1, 2, 3]),
map: new Map([['key', 'value']])
};
const deepCopy = structuredClone(original);
// 支持更多的数据类型
console.log(deepCopy.date instanceof Date); // true
console.log(deepCopy.set instanceof Set); // true
console.log(deepCopy.map instanceof Map); // true
// 处理循环引用
original.self = original;
const copyWithCircular = structuredClone(original);
console.log(copyWithCircular.self === copyWithCircular); // true
structuredClone的优点:
-
支持更多内置类型(Date、Set、Map、ArrayBuffer等)
-
正确处理循环引用
-
性能优化(底层使用结构化克隆算法)
局限性:
-
不支持函数、DOM节点
-
不支持原型链复制
-
兼容性:需要现代浏览器支持
3.2 手动实现深拷贝
基础版本
javascript
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型和null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理Date对象
if (obj instanceof Date) {
return new Date(obj);
}
// 处理正则表达式
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item, hash));
}
// 处理普通对象 - 检查循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
const clone = {};
hash.set(obj, clone);
// 复制所有属性(包括可枚举的Symbol属性)
const allKeys = [
...Object.keys(obj),
...Object.getOwnPropertySymbols(obj)
];
for (let key of allKeys) {
clone[key] = deepClone(obj[key], hash);
}
return clone;
}
增强版本:支持更多类型
javascript
function deepCloneEnhanced(target, hash = new WeakMap()) {
// 基本类型直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 处理特殊对象类型
const constructor = target.constructor;
// 检查循环引用
if (hash.has(target)) {
return hash.get(target);
}
let clone;
// 处理不同的对象类型
switch (constructor) {
case Date:
clone = new Date(target);
break;
case RegExp:
clone = new RegExp(target.source, target.flags);
break;
case Set:
clone = new Set();
hash.set(target, clone);
for (let value of target) {
clone.add(deepCloneEnhanced(value, hash));
}
break;
case Map:
clone = new Map();
hash.set(target, clone);
for (let [key, value] of target) {
clone.set(
deepCloneEnhanced(key, hash),
deepCloneEnhanced(value, hash)
);
}
break;
case ArrayBuffer:
clone = target.slice(0);
break;
case Int8Array:
case Uint8Array:
case Uint8ClampedArray:
case Int16Array:
case Uint16Array:
case Int32Array:
case Uint32Array:
case Float32Array:
case Float64Array:
clone = new constructor(target);
break;
case Function:
// 函数复制 - 注意这不会复制闭包中的变量
clone = eval('(' + target.toString() + ')');
break;
default:
// 普通对象或自定义类实例
if (target.prototype && target.prototype.constructor === target) {
// 这是一个构造函数,不复制
return target;
}
clone = Object.create(Object.getPrototypeOf(target));
hash.set(target, clone);
// 复制所有自有属性(包括Symbol)
const allKeys = [
...Object.getOwnPropertyNames(target),
...Object.getOwnPropertySymbols(target)
];
for (let key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor) {
if (descriptor.value && typeof descriptor.value === 'object') {
descriptor.value = deepCloneEnhanced(descriptor.value, hash);
}
Object.defineProperty(clone, key, descriptor);
}
}
}
return clone;
}
3.3 处理特殊情况的深拷贝
1. 循环引用处理
javascript
const obj = { a: 1 };
obj.self = obj; // 循环引用
// 错误的深拷贝会导致栈溢出
// const wrongCopy = JSON.parse(JSON.stringify(obj)); // 报错
// 正确的处理
function cloneWithCircular(obj, hash = new WeakMap()) {
if (hash.has(obj)) return hash.get(obj);
let clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = typeof obj[key] === 'object' && obj[key] !== null
? cloneWithCircular(obj[key], hash)
: obj[key];
}
}
return clone;
}
const safeCopy = cloneWithCircular(obj);
console.log(safeCopy.self === safeCopy); // true
2. 保持原型链
javascript
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
// 普通的扩展运算符会丢失原型链
const shallowPerson = { ...john };
console.log(shallowPerson instanceof Person); // false
// 保持原型链的深拷贝
function cloneWithPrototype(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const proto = Object.getPrototypeOf(obj);
const clone = Object.create(proto);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = typeof obj[key] === 'object' && obj[key] !== null
? cloneWithPrototype(obj[key])
: obj[key];
}
}
return clone;
}
const deepPerson = cloneWithPrototype(john);
console.log(deepPerson instanceof Person); // true
deepPerson.greet(); // "Hello, I'm John"
四、性能考量与优化策略
4.1 性能对比测试
javascript
// 性能测试函数
function measurePerformance(fn, data, iterations = 1000) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn(data);
}
const end = performance.now();
return end - start;
}
// 测试数据
const testData = {
nested: {
level1: {
level2: {
level3: {
data: Array(1000).fill('test')
}
}
}
}
};
// 测试不同拷贝方法的性能
console.log('JSON方法:', measurePerformance(
obj => JSON.parse(JSON.stringify(obj)), testData, 100
), 'ms');
console.log('手动深拷贝:', measurePerformance(
deepCloneEnhanced, testData, 100
), 'ms');
console.log('浅拷贝:', measurePerformance(
obj => ({ ...obj }), testData, 100
), 'ms');
4.2 性能优化策略
策略1:按需拷贝
javascript
// 只拷贝可能改变的部分
function selectiveClone(obj, keysToClone) {
const result = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (keysToClone.includes(key) && typeof obj[key] === 'object') {
result[key] = deepClone(obj[key]);
} else {
result[key] = obj[key];
}
}
}
return result;
}
策略2:使用不可变数据结构
javascript
// 使用Immutable.js库
import { Map, List } from 'immutable';
const original = Map({
user: Map({
name: '张三',
address: Map({
city: '北京'
})
})
});
// 修改不会影响原始数据
const updated = original.setIn(['user', 'name'], '李四');
console.log(original.getIn(['user', 'name'])); // 张三
console.log(updated.getIn(['user', 'name'])); // 李四
策略3:对象池技术
javascript
class ObjectPool {
constructor(createFn) {
this.createFn = createFn;
this.pool = [];
}
get() {
return this.pool.length > 0 ? this.pool.pop() : this.createFn();
}
release(obj) {
// 重置对象状态
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
this.pool.push(obj);
}
}
// 使用对象池
const userPool = new ObjectPool(() => ({
name: '',
age: 0,
address: null
}));
const user1 = userPool.get();
user1.name = '张三';
user1.age = 25;
// 使用后释放
userPool.release(user1);
五、实际应用场景
5.1 状态管理(如Redux、Vuex)
javascript
// Redux reducer中的状态更新
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state, // 浅拷贝第一层
todos: [
...state.todos, // 浅拷贝数组
{
id: action.id,
text: action.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed } // 浅拷贝并修改
: todo
)
};
default:
return state;
}
}
5.2 表单处理
javascript
class FormHandler {
constructor(initialData) {
this.initialData = deepClone(initialData);
this.currentData = deepClone(initialData);
}
// 修改表单字段
updateField(path, value) {
// 创建深拷贝以避免修改原始数据
const newData = deepClone(this.currentData);
// 使用路径访问嵌套属性
const keys = path.split('.');
let current = newData;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
this.currentData = newData;
return newData;
}
// 重置表单
reset() {
this.currentData = deepClone(this.initialData);
return this.currentData;
}
// 获取差异
getDiff() {
return this.deepDiff(this.initialData, this.currentData);
}
}
5.3 缓存优化
javascript
class CacheManager {
constructor() {
this.cache = new Map();
}
// 带缓存的深拷贝
deepCloneWithCache(obj) {
// 生成对象指纹(简单实现)
const fingerprint = JSON.stringify(obj);
if (this.cache.has(fingerprint)) {
console.log('缓存命中!');
return this.cache.get(fingerprint);
}
console.log('执行深拷贝...');
const clone = deepClone(obj);
this.cache.set(fingerprint, clone);
return clone;
}
// 清除缓存
clearCache() {
this.cache.clear();
}
}
六、最佳实践与总结
6.1 选择拷贝策略的决策流程

6.2 实践建议
-
优先使用语言内置方法:
-
对于简单对象,优先使用扩展运算符或Object.assign
-
对于现代浏览器环境,考虑使用structuredClone
-
对于简单数据序列化,JSON方法足够
-
-
避免过度拷贝:
-
只拷贝需要修改的部分
-
对于只读数据,考虑使用共享引用
-
使用不可变数据结构库处理复杂状态
-
-
注意性能影响:
-
深拷贝是昂贵的操作,避免在频繁调用的函数中使用
-
对于大型对象,考虑使用增量更新
-
使用缓存优化重复拷贝
-
-
处理边界情况:
-
始终考虑循环引用的可能性
-
注意特殊数据类型(函数、Symbol、BigInt等)
-
保持类型一致性
-
-
编写可维护的代码:
-
封装拷贝逻辑,提供统一接口
-
添加清晰的注释说明拷贝的深度和范围
-
编写单元测试覆盖各种边界情况
-
6.3 总结
深浅拷贝是编程中的基础但重要概念。正确的拷贝策略不仅能避免bug,还能优化性能。记住以下要点:
-
理解数据类型的本质:值类型和引用类型的区别是理解拷贝的基础
-
选择合适的方法:根据需求选择浅拷贝或深拷贝,根据数据类型选择实现方式
-
考虑边界情况:循环引用、特殊数据类型、性能问题都需要考虑
-
实践出真知:在实际项目中积累经验,形成自己的最佳实践
随着JavaScript语言的发展,新的API(如structuredClone)让深拷贝变得更简单。但理解其底层原理仍然重要,这样你才能在遇到问题时找到正确的解决方案。
希望这篇文章能帮助你更好地理解和使用深浅拷贝。在实际开发中,合理运用这些知识,将使你的代码更加健壮和高效。