美文网首页
超全面的自定义深拷贝函数

超全面的自定义深拷贝函数

作者: 一颗冰淇淋 | 来源:发表于2022-02-19 20:57 被阅读0次

    关于直接赋值、浅拷贝、深拷贝的区别和图示,之前写json的文章里有介绍,可以参考这一篇 json虽然简单,但这些细节你未必知道

    拷贝第一层

    首先,我们定义一个最简单的浅拷贝,只要能够保存原对象的第一层数据就行~

    function deepClone(value) { 
      const newObj = {};
       // 遍历原对象,将其赋值到新对象中
      for (let key in value) {
        newObj[key] = value[key];
      }
      return newObj;
    }
    
    // 定义一个对象
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
      },
    };
    
    // 拷贝出来的新对象
    const user = deepClone(obj);
    user.name = "kiki";
    user.friend.name = "kiki";
    
    console.log("原对象obj-", obj);
    console.log("新对象user-", user)
    

    修改新对象第一层属性name的值时,原对象是不会被修改的,但将新对象中第二层属性 friend.name 的值由 windy 修改为 kiki 时,原对象也被更改了

    1_拷贝第一层.png
    递归处理

    对象的属性中有可能还存在对象的情况,仅一层的判断是不够的,所以需要通过递归来处理,当递归到值不为对象时,直接返回

    // 判断传入的数据是否为对象
    function isObject(obj) {
      // 函数也是对象 
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value) {
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      const newObj = {};
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key]);
      }
      return newObj;
    }
    
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
    };
    
    const user = deepClone(obj);
    user.friend.name = "kiki";
    user.friend.address.city = 'Shanghai';
    
    console.log("原对象obj-", obj);
    console.log("新对象user-", user)
    

    递归调用后,拷贝的对象都是开辟了一块新的内存空间来保存,所以修改新对象的值,原对象不会变化。

    2_递归调用.png

    但我们发现一个新的问题,原对象中hobbies属性值类型为 "array",拷贝到新对象中变成了"object"。

    数组

    所以对于数组的处理和对象要区分开来,对传入的数据进行判断,如果为数组,创建用于复制的数据类型就应该为数组

    // 判断传入的数据是否为对象
    function isObject(obj) {
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value) {
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      // 判断value值是否为数组,为数组时创建新的数组
      const newObj = Array.isArray(value) ? [] : {};
      
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key]);
      }
      return newObj;
    }
    
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
      styding() {
        return "I am reading~";
      },
    };
    
    const user = deepClone(obj);
    console.log("原对象obj-", obj);
    console.log("新对象user-", user)
    

    此时对象和数组都可以正确的拷贝

    3_数组.png

    但我们在原对象中加了一个"studying方法",拷贝的新对象中"方法"变成了"空对象"

    方法

    函数本身就是为了能够复用,所以对象中的方法在进行深拷贝时,可以复用原来发方法,无需新创建,直接返回原方法即可

    // 判断传入的数据是否为对象
    function isObject(obj) {
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value) {
       // 当value为function时,直接返回原function
       if (typeof value === "function") return value;
       
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      // 判断value值是否为数组,为数组时创建新的数组
      const newObj = Array.isArray(value) ? [] : {};
      
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key]);
      }
      return newObj;
    }
    
    const symbolKey = Symbol("symbolKey");
    const symbolValue = Symbol("symbolValue");
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
      styding() {
        return "I am reading~";
      },
      [symbolKey]: "key",
      value: symbolValue,
    };
    
    const user = deepClone(obj);
    console.log("原对象obj-", obj);
    console.log("新对象user-", user)
    

    原对象和新对象的方法指向同一个内存地址,但我们一般也不需要对方法进行修改

    4_方法.png

    在原对象中增加了symbol分别作为key和value的场景,symbol属性作为key时,在新对象中直接丢失了

    Symbol

    Symbol作为key也是很常见的,避免key值重复,为了获取symbol所有的值,需要通过getOwnPropertySymbols,并添加到新对象中。

    // 判断传入的数据是否为对象
    function isObject(obj) {
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value) {
      // 当value为symbol时,返回一个新symbol
      if (typeof value === "symbol") return Symbol(value.description);
      
      // 当value为function时,直接返回原function
      if (typeof value === "function") return value;
      
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      // 判断value值是否为数组,为数组时创建新的数组
      const newObj = Array.isArray(value) ? [] : {};
      
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key]);
      }
      
      // 获取symbol为key的所有数据
      const symbols = Object.getOwnPropertySymbols(value);
      for (let sym of symbols) {
        newObj[sym] = deepClone(value[sym]);
      }
      return newObj;
    }
    
    const symbolKey = Symbol("symbolKey");
    const symbolValue = Symbol("symbolValue");
    const set = new Set([1, 2, 3, 4, 5]);
    const map = new Map([
      ["name", "alice"],
      ["age", 20],
    ]);
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
      styding() {
        return "I am reading~";
      },
      [symbolKey]: "key",
      value: symbolValue,
      set,
      map,
    };
    
    const user = deepClone(obj);
    console.log("原对象obj-", obj);
    console.log("新对象user-", user);
    

    symbol属性无论是作为key还是value,都能被成功拷贝

    5_symbol.png

    但新对象拷贝的map和set的值都变成了"空对象"

    map和set

    map和set作为value的场景比较少,所以这里就直接使用的浅拷贝,当值为map/set时,创建一个新的map/set,并返回。

    
    // 判断传入的数据是否为对象
    function isObject(obj) {
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value) {
      // 当value为set时,返回一个新set
      if (value instanceof Set) return new Set([...value]);
      
      // 当value为map时,返回一个新map
      if (value instanceof Map) return new Map([...value]);
      
      // 当value为symbol时,返回一个新symbol
      if (typeof value === "symbol") return Symbol(value.description);
      
      // 当value为function时,直接返回原function
      if (typeof value === "function") return value;
      
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      // 判断value值是否为数组,为数组时创建新的数组
      const newObj = Array.isArray(value) ? [] : {};
      
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key]);
      }
      
      // 获取symbol为key的所有数据
      const symbols = Object.getOwnPropertySymbols(value);
      for (let sym of symbols) {
        newObj[sym] = deepClone(value[sym]);
      }
      return newObj;
    }
    
    const symbolKey = Symbol("symbolKey");
    const symbolValue = Symbol("symbolValue");
    const set = new Set([1, 2, 3, 4, 5]);
    const map = new Map([
      ["name", "alice"],
      ["age", 20],
    ]);
    
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
      styding() {
        return "I am reading~";
      },
      [symbolKey]: "key",
      value: symbolValue,
      set,
      map,
    };
    
    const user = deepClone(obj);
    console.log("原对象obj-", obj);
    console.log("新对象user-", user);
    

    map和set也成功拷贝到新对象中了

    6_map&set.png
    循环引用

    对象是有可能存在循环引用,在window中就存在window属性,而且可以不断的调用。

    7_window.png

    如果需要拷贝的对象也是有属性指向自身的话,如 obj.info = obj。
    我们上面的深拷贝函数就会出现死循环,报 RangeError 栈溢出的错误。

    8_rangeError.png

    所以还需要对深拷贝的代码进行优化,定义一个map/weakMap用于保存传入的value值和新创建的对象值,每一次对函数调用时,先判断传入的value是否已经拷贝过,如果拷贝过,就直接返回之前拷贝的值,避免出现死循环。

    // 判断传入的数据是否为对象
    function isObject(obj) {
      return obj !== null && (typeof obj === "function" || typeof obj === "object");
    }
    
    function deepClone(value, map = new WeakMap()) {
      // 当value为set时,返回一个新set
      if (value instanceof Set) return new Set([...value]);
      
      // 当value为map时,返回一个新map
      if (value instanceof Map) return new Map([...value]);
      
      // 当value为symbol时,返回一个新symbol
      if (typeof value === "symbol") return Symbol(value.description);
      
      // 当value为function时,直接返回原function
      if (typeof value === "function") return value;
      
      // 当value不为对象时,直接返回value
      if (!isObject(value)) return value;
      
      // 当map中存在value时,直接返回map中的value
      if (map.has(value)) {
        return map.get(value);
      }
      
      // 判断value值是否为数组,为数组时创建新的数组
      const newObj = Array.isArray(value) ? [] : {};
      
      // 将函数接收到的value和新创建的obj保存到map中
      map.set(value, newObj);
      
      for (let key in value) {
        // 递归处理value值
        newObj[key] = deepClone(value[key], map);
      }
      
      // 获取symbol为key的所有数据
      const symbols = Object.getOwnPropertySymbols(value);
      for (let sym of symbols) {
        newObj[sym] = deepClone(value[sym], map);
      }
      return newObj;
    }
    
    const symbolKey = Symbol("symbolKey");
    const symbolValue = Symbol("symbolValue");
    const set = new Set([1, 2, 3, 4, 5]);
    const map = new Map([
      ["name", "alice"],
      ["age", 20],
    ]);
    
    const obj = {
      name: "alice",
      friend: {
        name: "windy",
        address: {
          city: "Beijing",
        },
      },
      hobbies: ["swiming", "dancing", "tennis"],
      styding() {
        return "I am reading~";
      },
      [symbolKey]: "key",
      value: symbolValue,
      set,
      map,
    };
    
    obj.info = obj;
    const user = deepClone(obj);
    console.log("原对象obj-", obj);
    console.log("新对象user-", user);
    
    9_循环引用.png

    以上就完成了自定深拷贝的所有步骤,使用深拷贝时,不用担心修改一处变量,另一处通过拷贝获取到的变量也被改变的情况,能够有效降低代码的bug率。

    关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~

    相关文章

      网友评论

          本文标题:超全面的自定义深拷贝函数

          本文链接:https://www.haomeiwen.com/subject/lfwplrtx.html