关于深拷贝的一些总结

64 阅读5分钟

前置知识

前置认知很杂但是却很重要(^_^),站在巨人的肩膀上才能看的更远

  • Js的数据类型

    分为基础数据类型:string、number、boolean、null、undefinde、symbol

    引用类型:Object(大类array、function、data等都归属于Object)

    基础数据类型存放在栈内存中,而引用数据类型因为大小不确定,所以存放在堆内存中,但引用类型、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、会在栈中存储一个指针,这个指针指向堆内存空间中该实体的起始地址。堆栈内存

深拷贝的定义

知道了Js的数据类型,那么接下来就能看下对应的拷贝操作了

对于基础数据类型,它的拷贝其实就是在栈内存中直接新开一个空间进行,所以基础数据类型是没有什么深拷贝的概念

而对于引用类型,传统的赋值方法,只不过是复制了引用类型存放在栈内存中的一个地址指针,并没有在内存中新开一个区域,属于浅拷贝。

深拷贝的实现方式

JSON.parse(JSON.stringify())

非常简单的一种写法,能够满足大多数的应用场景。但是这种方案会忽略undefined、symbol、function,也不能解决循环引用问题, NAN, Infinity会被序列化为null。

    let obj = {
        a: undefined,
        b: Symbol('e'),
        c: function() {},
        d: NaN
    }
    let newObj = JSON.parse(JSON.stringify(obj))
    newObj // { d: null }

ps:接下来了解一下JSON.parse 和 JSON.stringfy (序列化和反序列化)

JSON.stringfy

  • 语法: JSON.stringify(value[, replacer [, space]])

  • 参数含义

    • value:将要序列化成 一个 JSON 字符串的值。

    • replacer(可选)

      1. 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
      2. 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
      3. 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
    • space(可选)指定缩进用的空白字符串,用于美化输出

  • 返回值: 一个给定值的JSON字符串

  • 异常: 当在循环引用时会抛出异常 TypeError ("cyclic object value")(循环对象值)

  • 例子

    console.log( 
    JSON.stringify({ name: 'hbw', sex: 'boy', age: 24 }, (key, value) => { return typeof value === 'number' ? undefined : value }) ) 
    // '{"name":"hbw","sex":"boy"}'
    
    console.log(JSON.stringify({ x: 5, y: '你好' }, null, 'hbw'))
    /*{
        hbw"x": 5,
        hbw"y": "你好"
        }
    */
    

JSON.parse

  • 语法: JSON.parse(text[, reviver])
  • 参数
    • text:要被解析成 JavaScript 值的字符串
    • revier(可选): 转换器,如果传入该参数 (函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前

MDN-JSON.parse

手写简单版的深拷贝

思路很简单,创建一个新对象,遍历需要克隆的对象,将其属性添加到新对象上。(需要注意的是,因为不知道克隆对象的深度,所以需要递归处理)

    function clone(obj) {
        if (typeof obj = 'object') {
            let newObj = Array.isArray(obj) ? [] : {};
            for (let key in obj){
                newObj[key] = clone(obj[key]);
            }
            return newObj;
        }
        return obj;
    }

当然这个版本的深拷贝,不能解决循环引用的问题。 为了解决循环引用问题,可以可以考虑用一个新的存储空间来记录当前对象和拷贝对象的关系,所以当需要拷贝当前对象时,先去开辟的存储空间内寻找,避免了循环引用的问题。

JS中的WeakMap就是一个完美的数据结构用来当作存储空间。

WeakMap是一组键/值对的集合,其中的键是弱引用(弱引用不会被认可强弱引用)的。其键必须是对象,而值可以是任意的。

(为何使用WeakMap主要是利用了弱引用的特性,不用手动清除WeakMap的属性,可以让浏览器自己主动释放内存)

   function supClone(obj, wm = new WeakMap()) {
        if (typeof obj === 'object') {
            let newObj = Array.isArray(obj) ? [] : {};
            if (wm.get(obj)) {
                return wm.get(obj);
            }
            wm.set(obj, newObj);
            for (let key in obj) {
                newObj[key] = supClone(obj[key], wm);
            }
            return newObj;
        } else {
            return obj;
        }
    }

至此实现了一个简单版的深拷贝,但是这个版本的深拷贝还是有很多问题

  1. 使用typeof进行类型判断不够准确,typeof会将null判断为object类型,将函数判断为function
  2. 没有对引用类型进行精确划分,简单版中只区分了数组类型Arry,但是包括像RegExp、Map等不能准确区分
  3. 在创建新的对象时,我们直接使用了字面量来创建let newObj = Array.isArray(obj) ? [] : {};这样的话会导致原型丢失。

为了准确判断引用类型,可以使用Object.prototype.toString.call

为了保留对象原型上的数据可以使用原对象的构造方法

   function isObject(target) { 
       const type = typeof target; 
       return target !== null && (type === 'object' || type === 'function'); 
   }
    
   function getDType(val) { 
       return Object.prototype.toString.call(val).replace(/\[object (\w+)\]/, '$1') .toLowerCase(); 
   }

   function deepClone(obj, wm = new WeakMap()) {
        // 原始数据类型
        if (!isObject(obj)) {
            return obj;
        }
        
        if (wm.get(obj)) {
            return wm.get(obj);
        }
        
        const type = getDType(obj);
        
        if (['array', 'object'].includes(type)){
           let newObj = new obj.constructor();
           wm.set(obj, newObj);
           
           for (const key of obj) {
              newObj[key] = deepClone(obj, wm);
           }
           return newObj
        }
        // 处理map
        if (type === 'map') {
            const cloneTarget = new Map(); 
            wm.set(target, cloneTarget); 
            for (const [key, val] of target) { 
                cloneTarget.set(key, deepClone(val, wm)); 
            } 
            return cloneTarget;
        }
        //像正则、Error、函数、Date这种一般不会发生变化的直接返回就可以
        return obj;
   }

总结

一个深拷贝可以简单的用一行代码就能解决问题,但是当需要考虑到更全面的情况下就会复杂起来。 代码如此,人生亦如此。不必焦虑,一步步往前即可,只要确定自己的方向是正确的就行呀(o^^o)(走在正确的道路上🤒)。