深拷贝
数据类型存储
在JavaScript中有两大数据类型:
- 基本类型:存储在栈内存中。
- 引用类型:存储在堆内存中,在栈上的变量的值是堆内存中实际对象的引用。
什么是深拷贝
深拷贝是程序设计中一种使对象复制过程中,不仅仅复制对象本身,还包括对象所引用的所有对象的复制方法。这意味着通过深拷贝生成的新对象与原始对象在内存中完全独立,更改新对象不会影响原始对象。
- 复制所有层次的对象:深拷贝不仅复制最顶层的对象,还复制对象内部所有层次的子对象。例如,如果有一个包含其他对象或列表的对象,深拷贝会递归地复制所有内部对象和列表。
- 内存中的独立性:深拷贝生成的对象在内存中与原始对象有完全不同的地址。这意味着对新对象或其任何子对象的修改都不会影响原始对象。
- 处理循环引用:在对象之间存在循环引用时,深拷贝算法能够正确地处理这些情况,防止无限递归复制。深拷贝通过维护一个已复制对象的引用字典来实现这一点,从而确保每个对象只被复制一次。
常见的深拷贝方式有:
_.cloneDeep()
jQuery.extend()
JSON.stringify()
,但是这种方式会忽略undefined
、symbol
和函数。- 手写递归深拷贝
手写循环递归
function deepClone(obj) {
const hash = new Map(); // 创建一个新的Map对象,用来存储已经访问过的对象,防止循环引用。
function clone(obj) {
// 如果obj不是对象或者是null,直接返回该值。这处理了基本数据类型和null
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 检查hash中是否已经存储了当前对象的拷贝,如果是,直接返回该拷贝
// 这一步是处理循环引用的关键。
if (hash.has(obj)) {
return hash.get(obj);
}
let newObj;
// 根据对象的具体类型来初始化newObj。对于不同类型的对象,我们需要不同的处理方式
if (obj instanceof Array) {
newObj = []; // 如果是数组,创建一个空数组
} else if (obj instanceof Object) {
newObj = {}; // 如果是普通对象,创建一个空对象
} else if (obj instanceof Date) {
newObj = new Date(obj); // 如果是Date对象,创建一个新的Date对象
} else if (obj instanceof RegExp) {
newObj = new RegExp(obj); // 如果是RegExp对象,创建一个新的RegExp对象
}
// 存储当前对象到hash中,记录其拷贝。这样在后续递归中如果再次遇到此对象,可以直接返回其拷贝
hash.set(obj, newObj);
// 遍历obj的所有自身属性(包括symbol类型的属性),递归地拷贝每个属性的值
Reflect.ownKeys(obj).forEach(key => {
newObj[key] = clone(obj[key]);
});
return newObj; // 返回新对象,该对象是原始对象的深拷贝
}
return clone(obj); // 调用内部定义的clone函数开始递归复制过程
}
tip
如果代码中没有这一部分:
if (hash.has(obj)) {
return hash.get(obj);
}
可能会导致以下问题:
1. 循环引用问题:
在 JavaScript 中,对象可以包含对自身或其他对象的引用,这被称为循环引用。例如:
let obj1 = {};
let obj2 = { obj1 };
obj1.obj2 = obj2;
在这种情况下,如果你试图对 obj1
进行深拷贝,代码将会进入无限递归,因为 obj1
引用了 obj2
,而 obj2
又引用了 obj1
。最终,这种无限递归会导致堆栈溢出错误(Stack Overflow
)。
这个检查(hash.has(obj)
)的目的是防止这种情况。当检测到对象已经被拷贝 过,它会直接返回该对象的拷贝,而不是继续递归拷贝,这样就避免了无限递归。
2. 重复拷贝问题:
如果没有这部分代码,即使没有循环引用的情况下,对于多次出现的相同对象(即同一个对象在原对象中被多次引用),代码会重复拷贝该对象多次。虽然这不会导致错误,但会浪费内存并且性能较差。
例如:
let commonObj = { a: 1 };
let obj = { obj1: commonObj, obj2: commonObj };
没有上述检查的情况下,commonObj
会被深拷贝两次,而有了检查之后,它只会被拷贝一次,obj1
和 obj2
都会引用同一个拷贝。