我用租房合同的比喻帮你彻底理解这个问题
一、生活化解释(租房合同版)
1. 浅拷贝 - 二房东转租
javascript
// 原始租房合同
const originalContract = {
address: "杭州西湖101室",
items: ["空调", "沙发"], // 共用家具
landlord: { name: "王阿姨" } // 共用房东信息
};
// 二房东做浅拷贝转租
const shallowCopy = { ...originalContract };
shallowCopy.address = "杭州西湖101室(隔断间)"; // ✅ 修改自己的地址
shallowCopy.items.push("洗衣机"); // ❌ 实际修改了原始合同的家具
现象 :原始合同的items
数组被修改,所有租客共享同一份家具清单
2. 深拷贝 - 重新签订合同
javascript
// 使用深拷贝生成新合同
function deepClone(obj) { /* 稍后实现 */ }
const newContract = deepClone(originalContract);
newContract.items.push("电视机"); // ✅ 只影响新合同
结果:新旧合同完全独立,互不影响
二、代码案例对比
浅拷贝实现方式
javascript
// 案例对象
const school = {
name: "浙江大学",
departments: ["计算机系", "电子系"],
info: { location: "北京" }
};
// 方法1:展开运算符
const shallowCopy1 = { ...school };
// 方法2:Object.assign
const shallowCopy2 = Object.assign({}, school);
// 测试修改
shallowCopy1.departments.push("数学系");
console.log(school.departments); // ["计算机系", "电子系", "数学系"]
三、手写深拷贝(详细注释版)
javascript
function deepClone(target, map = new WeakMap()) {
// 基本类型直接返回(终止递归的条件)
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理循环引用:如果已经拷贝过该对象,直接返回存储的副本
if (map.has(target)) {
return map.get(target);
}
// 处理特殊对象类型
const constructor = target.constructor;
// Date类型:new Date(value)
if (target instanceof Date) {
return new Date(target);
}
// RegExp类型:new RegExp(source, flags)
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 创建新容器(数组/对象)
const clone = Array.isArray(target) ? [] : {};
// 记录当前对象与拷贝对象的映射关系(解决循环引用)
map.set(target, clone);
// 递归拷贝所有属性
for (let key in target) {
// 只拷贝对象自身的属性(不拷贝原型链上的属性)
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
测试用例
javascript
const original = {
name: "测试对象",
features: ["嵌套", { data: "结构" }],
created: new Date(),
regex: /abc/gi,
circular: null // 准备设置循环引用
};
// 设置循环引用
original.circular = original;
// 执行深拷贝
const cloned = deepClone(original);
// 修改拷贝对象
cloned.features[1].data = "修改后的数据";
console.log(original.features[1].data); // 仍然是"结构"
console.log(cloned.circular === cloned); // true(保持循环引用结构)
四、核心区别分析
对比维度 | 浅拷贝 | 深拷贝 |
---|---|---|
复制层级 | 只复制第一层属性 | 递归复制所有嵌套层级 |
内存关系 | 嵌套对象共享内存地址 | 所有层级都创建新内存空间 |
修改影响 | 修改嵌套属性会影响原对象 | 完全独立,互不影响 |
性能消耗 | 快(O(n)) | 慢(O(n²)) |
适用场景 | 简单数据结构的快速复制 | 需要完全隔离的复杂对象 |
处理循环引用 | 无法处理 | 通过WeakMap记录映射关系 |
特殊对象处理 | 直接复制引用 | 需要特殊处理Date/RegExp等类型 |
五、开发中的经典应用场景
需要浅拷贝的场景
javascript
// 场景:合并默认配置
const defaultConfig = { timeout: 3000, retry: 3 };
const userConfig = { timeout: 5000 };
// 正确做法:浅拷贝合并
const finalConfig = { ...defaultConfig, ...userConfig };
// { timeout: 5000, retry: 3 }
必须用深拷贝的场景
javascript
// 场景:保存游戏进度
const currentGameState = {
player: { level: 5, items: ["剑", "盾"] },
enemies: [{ type: "orc" }, { type: "dragon" }]
};
// 正确做法:深拷贝存档
const savedGame = deepClone(currentGameState);
// 修改当前游戏状态不会影响存档
currentGameState.player.level = 6;
console.log(savedGame.player.level); // 仍然是5
六、如何选择拷贝方式?
-
优先考虑浅拷贝:
- 当数据没有嵌套结构时
- 需要高性能处理的简单数据
- 明确知道只需要修改第一层属性
-
必须使用深拷贝:
- 处理来自外部的数据(防止数据污染)
- 需要持久化存储的状态
- 在状态管理(如Redux)中修改嵌套状态
- 处理包含多个嵌套层级的配置对象
建议:当不确定数据结构时,使用深拷贝更安全!但要注意对特殊类型(如函数、DOM元素)的处理需要额外逻辑。