简单记录-实现简单深拷贝

87 阅读4分钟

深拷贝、浅拷贝

  • 浅拷贝: 拷贝的是对象的指针,修改内容互相影响。
  • 深拷贝: 整个对象拷贝到另一个内存中,修改内容互不影响。

深拷贝实现

常见

不需要借助其他工具的情况下,我们想要实现一个深拷贝的话,最常用的应该是这样的:

 JSON.parse(JSON.stringify());

入手

  • 拷贝的数据是基础数据类型:无需继续拷贝,直接返回
  • 循环引用
  • 处理引用类型
  • 拷贝对象深度未知:层级递归
  * 如果是基础数据类型,直接return
  * 如果是引用数据类型,处理ObjectArray等
  * @param target
  */
 const deepClone = <T extends unknown>(target: T) : T =>{
     if (typeof target === 'object') {
         // 处理数组
         if (Array.isArray(target)) {
             return target.map(item=> deepClone(item)) as T;
         }
         // 非 数组,创建新拷贝对象
         const cloneTarget : T = {} as T;
         for (let _key in target) {
             cloneTarget[_key] = deepClone(target[_key])
         }
         return cloneTarget;
     }
     return target
 ​
 }
 ​

执行用例

 const target = {
   test1: 1,
   test2: undefined,
   test3: {
     child: '123',
   },
   test4: [{
     test1: '123',
     test2: '123',
     test3: [1, 2, 3],
   }],
 };
 const a = target;
 // 拷贝前尝试修改值
 a.test1 = 666;
 const b = deepClone(target);
 // 拷贝后修改值
 b.test1 = 999;
 console.log('拷贝前尝试修改值', a);
 console.log('拷贝后修改值', b);
 console.log('最终的target', target);

image-20220924131849641

通过控制台打印可以发现,在作出深拷贝前,修改了target的值,然后再通过深拷贝,进而修改值,其最终打印的target只是原来修改的值。

测试用例-循环引用

 target.target = target;
 const b = deepClone(target);
 console.log('拷贝后修改值', b);

image-20220924132511653

好家伙,栓Q了,因为递归进入死循环导致栈内存溢出了,其原因大概就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况,因此,还需要进一步优化深拷贝代码。

  • 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
  • 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构。
 /**
  * 如果是基础数据类型,直接return
  * 如果是引用数据类型,处理Object、Array等
  * @param target
  */
 // 开辟存储空间
 const targetMap = new Map();
 const deepClone = <T extends unknown>(target: T): T => {
   if (typeof target === 'object') {
     // 处理数组
     if (Array.isArray(target)) {
       return target.map((item) => deepClone(item)) as T;
     }
     // 非 数组,创建新拷贝对象
     const cloneTarget: T = {} as T;
     // 有数据,直接返回
     if (targetMap.get(target)) {
       return targetMap.get(target);
     }
     // 否则,存储
     targetMap.set(target, cloneTarget);
     for (const _key in target) {
       cloneTarget[_key] = deepClone(target[_key]);
     }
     return cloneTarget;
   }
   return target;
 };
 export { deepClone };
 ​

image-20220924133831600

此时可以发现已经不会出现溢出情况。

使用WeakMap

weakMap(个人理解:弱的Map)和Map有了解过的应该都知道,MapWeakMap 是两种数据结构,可用于操纵键和值之间的关系。

但是,WeakMap 仅接受对象。这意味着我们不能将基本类型用作 WeakMap 的键。与 Map不同,WeakMap 不支持对键和值进行迭代。无法获取 WeakMap 的所有键或值。此外,也没有办法清除 WeakMapWeakMap 不会阻止在没有对键的引用时对键进行垃圾收集。

Map 可以无限期地维护对键和值的引用。一旦创建了键和值,它们将占用内存,即使没有对它们的引用,也不会被垃圾收集。这可能会导致内存泄漏问题。

由于 WeakMap 保存对键的弱引用,且无法枚举,因此无法使用 keys()values()entries() 这些方法。

 /**
  * 如果是基础数据类型,直接return
  * 如果是引用数据类型,处理Object、Array等
  * @param target
  */
 // 开辟存储空间
 const targetMap = new WeakMap();
 const deepClone = <T extends unknown>(target: T): T => {
   if (typeof target === 'object' && target !== null) {
     // 处理数组
     if (Array.isArray(target)) {
       return target.map((item) => deepClone(item)) as T;
     }
     // 非 数组,创建新拷贝对象
     const cloneTarget: T = {} as T;
     // 有数据,直接返回
     if (targetMap.get(target)) {
       return targetMap.get(target);
     }
     // 否则,存储
     targetMap.set(target, cloneTarget);
     for (const _key in target) {
       cloneTarget[_key] = deepClone(target[_key]);
     }
     return cloneTarget;
   }
   return target;
 };
 ​

致谢

由于学识有限,如若文中有不足之处,欢迎批评指正!