深拷贝与浅拷贝详解

86 阅读4分钟
  • 首先,只有引用数据类型(数组,对象)才存在深拷贝和浅拷贝。

<!DOCTYPE html>
<html>
  <head>
   
  </head>
  <body>
    <h1>深拷贝与浅拷贝</h1>
    <script>
        let arr = [1,2,3,4,5];
        // = 赋值
        let newArr = arr;
        newArr.push(6);
        // console.log(arr, newArr)

        // 基本类型不存在深浅拷贝
        let a = 1;
        let b = a;
        b = 2;
        // console.log(a)

        // 解构赋值是深拷贝还是浅拷贝,为什么?
        // 针对 一维数组,一维对象 => (伪深拷贝)true false = false
        let arr2 = [[1,2],[2,3],[3,4]];
        let newArr2 = [...arr2]; // ES6 => 解构赋值
        newArr2[0].push(666);
        // console.log(arr2, newArr2) 

        let obj = {
          a:{aa:1, bb:2},
          b:3,
          c: 4,
        }

        //stringify => 对象转字符串 
        // 最快捷的深拷贝方法 80%的场景
        // let newObj = JSON.parse(JSON.stringify(obj));
        // newObj.a.aa = 666;

        // function => "function"

        // console.log(obj, newObj)

        // 手写 深拷贝
        function deepClone(source){
          // 容器
          const targetObj = source.constructor === Array ? [] : {};
          
          for(let keys in source){
            if(source.hasOwnProperty(keys)){
              // object => 对象,数组
              if(source[keys] && typeof source[keys] === "object"){
                targetObj[keys] = source[keys].constructor === Array ? [] : {};
                targetObj[keys] = deepClone(source[keys])
              }else{
                // 基本类型
                targetObj[keys] = source[keys];
              }
            }
          }
          return targetObj;
        }

        // obj => Object [] => Array  // 基类

        let obj2 = {
          a:{aa:1, bb:2},
          b:3,
          c: 4,
        }

        let newObj222 = deepClone(obj2);
        newObj222.a.bb = 6688;
        console.log(obj2, newObj222)

    </script>
  </body>
</html>
  • 为什么要判断source[keys]是否为空?

    • 判断source[keys]是否为空是为了确保在深拷贝过程中不会出现错误。如果不进行判断,当source[keys]为空或undefined时,尝试访问其constructor属性(比如source[keys].constructor)会导致TypeError。

    • 通过判断source[keys]是否为空,可以避免在深拷贝过程中对空对象或空数组进行递归复制操作,从而防止出现错误。如果source[keys]为空,就直接将其赋值给targetObj[keys],不进行进一步的深拷贝操作。

    • 需要注意的是,这里的判断仅限于空对象或空数组。如果source[keys]是其他类型的数据(比如基本类型),则不会进行进一步的判断,而是直接将其赋值给targetObj[keys]。这是因为基本类型的数据无法进行深拷贝,只能通过直接赋值来实现拷贝。

    • source[keys]为空时,可以理解为在原对象或数组中,某个属性或元素的值是空或未定义。

    • 例如,假设有以下的原始对象:

    let obj = {
        a: { aa: 1, bb: 2 },
        b: null,
        c: undefined,
    };
    
    • 如果不进行判断,直接对obj进行深拷贝,那么在复制a属性的过程中,会出现错误。因为obj.a存在并且是一个非空对象,但在复制obj.a时,会尝试访问obj.a.constructor,由于obj.a是一个空对象,没有constructor属性,所以会抛出TypeError。

    • 通过判断source[keys]是否为空,我们可以避免这个错误,将空对象直接赋值给新对象的对应属性,而不进行进一步的复制操作。

    • 修改后的例子如下:

    function deepClone(source) {
      const targetObj = source.constructor === Array ? [] : {};
    
      for (let key in source) {
        if (source.hasOwnProperty(key)) {
          if (source[key] && typeof source[key] === "object") {
            // 判断source[key]是否为空
            if (source[key] === null || source[key] === undefined) {
              targetObj[key] = source[key];
            } else {
              targetObj[key] = deepClone(source[key]);
            }
          } else {
            targetObj[key] = source[key];
          }
        }
      }
    
      return targetObj;
    }
    
    let obj = {
      a: { aa: 1, bb: 2 },
      b: null,
      c: undefined,
    };
    
    let newObj = deepClone(obj);
    console.log(newObj); // { a: { aa: 1, bb: 2 }, b: null, c: undefined }
    
    • 在这个例子中,当遍历到obj.b时,判断它为空,直接将其赋值给targetObj.b。同样地,当遍历到obj.c时,也将其空值赋给targetObj.c。这样就确保了在深拷贝过程中不会出现对空对象或空数组进行递归复制的错误。
  • 为什么无法拷贝函数?

    • JSON.stringify()JSON.parse() 方法都是用于处理 JSON 数据的,而在 JSON 格式中,函数是无法被表示或序列化的。因此,当使用 JSON.stringify() 方法将对象转换为 JSON 字符串时,任何函数属性都会被忽略掉,不会包含在结果字符串中。

    • 例如以下对象:

    let obj = {
        a: 1,
        b: "Hello",
        c: function() {
          console.log("Function");
        }
    } 
    
    • 如果我们尝试使用 JSON.stringify(obj),得到的结果将是 { "a": 1, "b": "Hello" },可以看到函数属性 c 被完全忽略了。这是因为 JSON 格式只支持基本数据类型、对象和数组,并且不能包含函数、正则表达式、日期对象等特殊类型。

    • 同样地,当使用 JSON.parse() 方法解析一个 JSON 字符串时,其中的函数属性也会被忽略,无法重新创建实际的函数。

  • source.hasOwnProperty(keys)的作用

    • source.hasOwnProperty(keys) 是用于检查一个对象是否拥有指定的属性(不包括原型链上的属性)。它是 JavaScript 中的内置方法。

    • 在深拷贝函数中使用 source.hasOwnProperty(keys) 是为了确保只处理对象自身的属性,而不包括继承自原型链的属性。这样可以避免在深拷贝过程中复制原型链上的属性,从而保持新对象的独立性。

    • 例如,考虑以下对象和其对应的原型链:

    let parent = {
      a: 1,
      b: 2,
    };
    
    let child = Object.create(parent);
    child.c = 3;
    
    • 在这个例子中,child 对象继承了 parent 对象的属性 ab。如果直接对 child 进行深拷贝,而没有使用 source.hasOwnProperty(keys) 进行判断,那么在拷贝过程中会将 parent 对象的属性也复制到新对象中,导致新对象与原对象共享这些属性。

    • 通过使用 source.hasOwnProperty(keys) 来判断,可以确保只处理 child 对象自身的属性,从而创建一个与原对象完全独立的新对象。