Skip to main content

深拷贝

数据类型存储

在JavaScript中有两大数据类型:

  • 基本类型:存储在栈内存中。
  • 引用类型:存储在堆内存中,在栈上的变量的值是堆内存中实际对象的引用。

什么是深拷贝

深拷贝是程序设计中一种使对象复制过程中,不仅仅复制对象本身,还包括对象所引用的所有对象的复制方法。这意味着通过深拷贝生成的新对象与原始对象在内存中完全独立,更改新对象不会影响原始对象。

  1. 复制所有层次的对象:深拷贝不仅复制最顶层的对象,还复制对象内部所有层次的子对象。例如,如果有一个包含其他对象或列表的对象,深拷贝会递归地复制所有内部对象和列表。
  2. 内存中的独立性:深拷贝生成的对象在内存中与原始对象有完全不同的地址。这意味着对新对象或其任何子对象的修改都不会影响原始对象。
  3. 处理循环引用:在对象之间存在循环引用时,深拷贝算法能够正确地处理这些情况,防止无限递归复制。深拷贝通过维护一个已复制对象的引用字典来实现这一点,从而确保每个对象只被复制一次。

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify(),但是这种方式会忽略undefinedsymbol和函数。
  • 手写递归深拷贝

手写循环递归

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 会被深拷贝两次,而有了检查之后,它只会被拷贝一次,obj1obj2 都会引用同一个拷贝。