JavaScript中的拷贝问题:深入理解深浅拷贝
在JavaScript开发中,拷贝对象是常见的操作,但深浅拷贝的区别常常让开发者困惑。本文将深入探讨拷贝的机制、方法和陷阱。
一、JavaScript数据类型与存储机制
数据类型分类
- 基本类型:Number、String、Boolean、null、undefined、Symbol、BigInt
- 引用类型:Object、Array、Function、Date等
内存存储方式
- 栈内存:存储基本类型值和引用类型的地址指针
- 堆内存:存储引用类型的实际数据
// 基本类型存储在栈中
let a = 10;
let b = a; // 创建值的副本
a = 20;
console.log(b); // 10 - b不受a影响
// 引用类型存储在堆中
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 复制引用地址
obj1.name = 'Bob';
console.log(obj2.name); // 'Bob' - 两者共享同一内存
二、浅拷贝:只复制表层
浅拷贝只复制对象的第一层属性,当属性是引用类型时,复制的是地址引用。
常用浅拷贝方法
// 1. Object.assign()
let obj = { a: 1, b: { c: 2 } };
let shallow1 = Object.assign({}, obj);
// 2. 扩展运算符
let shallow2 = { ...obj };
// 3. Array.prototype.concat()
let arr = [1, 2, { d: 3 }];
let shallow3 = [].concat(arr);
// 4. Array.prototype.slice()
let shallow4 = arr.slice();
// 5. Array.from()
let shallow5 = Array.from(arr);
浅拷贝的陷阱
let original = {
name: 'John',
hobbies: ['reading', 'swimming']
};
let copy = { ...original };
// 修改原始对象的基本类型属性
original.name = 'Mike';
console.log(copy.name); // 'John' - 不受影响
// 修改原始对象的引用类型属性
original.hobbies.push('running');
console.log(copy.hobbies); // ['reading', 'swimming', 'running'] - 被影响!
三、深拷贝:完全克隆对象
深拷贝创建对象及其嵌套对象的完全独立副本,新旧对象互不影响。
常用深拷贝方法
// 1. JSON方法(最常用但有局限)
let deep1 = JSON.parse(JSON.stringify(obj));
// 2. structuredClone()(现代浏览器支持)
let deep2 = structuredClone(obj);
JSON方法的局限性
let obj = {
a: undefined, // 丢失
b: Symbol('id'), // 丢失
c: function() {}, // 丢失
d: new Date(), // 转为字符串
e: BigInt(100), // 报错
f: new Map(), // 转为空对象
g: new Set(), // 转为空对象
h: obj // 循环引用报错
};
let clone = JSON.parse(JSON.stringify(obj));
console.log(clone);
// 输出: { d: "2023-08-17T12:34:56.789Z" }
四、手写深拷贝函数
基础版本:处理基本类型和普通对象
function deepClone(target) {
// 基本类型直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理数组
const clone = Array.isArray(target) ? [] : {};
for (let key in target) {
// 只复制自有属性(非原型链上的属性)
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key]);
}
}
return clone;
}
进阶版本:处理特殊对象和循环引用
function deepClone(target, map = new WeakMap()) {
// 基本类型直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理循环引用
if (map.has(target)) {
return map.get(target);
}
// 初始化克隆对象
let clone;
// 处理特殊对象类型
switch (true) {
case Array.isArray(target):
clone = [];
break;
case target instanceof Date:
clone = new Date(target);
break;
case target instanceof Map:
clone = new Map();
target.forEach((value, key) => {
clone.set(key, deepClone(value, map));
});
break;
case target instanceof Set:
clone = new Set();
target.forEach(value => {
clone.add(deepClone(value, map));
});
break;
case target instanceof RegExp:
clone = new RegExp(target.source, target.flags);
break;
default:
clone = Object.create(Object.getPrototypeOf(target));
}
// 记录已拷贝对象
map.set(target, clone);
// 递归复制属性
for (let key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
测试深拷贝函数
const original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'traveling'],
meta: {
created: new Date(),
tags: new Set(['js', 'web']),
settings: new Map([['theme', 'dark'], ['notifications', true]])
},
getInfo() {
return `${this.name}, ${this.age}`;
}
};
// 创建循环引用
original.self = original;
const clone = deepClone(original);
// 修改原始对象
original.hobbies.push('swimming');
original.meta.settings.set('theme', 'light');
console.log(clone.hobbies); // ['reading', 'traveling']
console.log(clone.meta.settings.get('theme')); // 'dark'
console.log(clone.self === clone); // true (循环引用正确处理)
五、拷贝方法对比指南
| 方法/特性 | 浅/深拷贝 | 处理函数 | 处理Symbol | 处理循环引用 | 性能 |
|---|---|---|---|---|---|
| 赋值(=) | 无拷贝 | ✓ | ✓ | ✓ | 最高 |
| 扩展运算符(...) | 浅拷贝 | ✓ | ✓ | ✓ | 高 |
| Object.assign() | 浅拷贝 | ✓ | ✓ | ✓ | 高 |
| Array.prototype.slice | 浅拷贝 | ✓ | ✓ | ✓ | 高 |
| JSON方法 | 深拷贝 | ✗ | ✗ | ✗ | 中 |
| structuredClone() | 深拷贝 | ✗ | ✗ | ✓ | 中 |
| 手写深拷贝函数 | 深拷贝 | ✓ | ✓ | ✓ | 低 |
六、最佳实践建议
- 优先使用浅拷贝:当对象没有嵌套引用类型时
- 使用JSON方法:处理简单数据结构时(无特殊类型和循环引用)
- 使用structuredClone():现代项目中处理较复杂对象
- 实现自定义深拷贝:需要处理函数、Symbol等特殊类型时
- 避免拷贝大对象:考虑性能影响,尤其递归深拷贝
总结
理解深浅拷贝的区别是JavaScript开发的基本功。浅拷贝适合简单对象,而深拷贝在需要完全隔离对象时必不可少。现代浏览器提供的structuredClone()是个不错的选择,但对于复杂场景,实现自定义深拷贝函数仍是必要的技能。根据具体需求选择合适的方法,才能在性能和功能间取得最佳平衡。