深拷贝和浅拷贝

458 阅读6分钟

什么是深拷拷贝浅拷贝

数据类型分为基本数据类型和引用数据类型,两种数据类型的存储方式不一样:

  • 基本数据类型:将数据直接存储在栈里面。
  • 引用数据类型:栈里面存放的是地址,数据存储在堆内存里,地址指向堆内存里的数据。

深拷贝浅拷贝指的是在复制引用数据类型(下面统指对象)时的两种不同的拷贝方式。

1. 浅拷贝

创建一个新的对象,新对象和原始对象的属性相同,如果属性是基本数据类型,那么拷贝的就是基本数据类型存储在栈里的数据,两个对象的该属性之间不会相互影响,是独立的。如果属性是引用数据类型,那么拷贝的就是引用数据类型存储在栈里的地址,指向堆数据里同一个数据,所以两个对象的该属性之间是会相互影响的。

可以理解为只拷贝第一层的数据,第二层以后就是拷贝引用地址了。

换做数组就是,如果元素是基本数据类型,拷贝的就是基本数据类型存在栈里的数据,两个数组的该元素不互相影响。如果元素是引用数据类型,拷贝的就是地址,因此如果该元素的值发生改变,那么另外一个数组的该元素也会随之发生改变。

    let obj1 = {
      name: "于家宝",
      body: {
        age: 18,
      },
    };
    // 拓展运算符是浅拷贝的一种方式
    let obj2 = { ...obj1 };
 
    obj1.name = "yujiabao"; // 该属性name为基本数据,拷贝的是真实数据,因此两个对象的该属性不会相互影响
    obj1.body.age = 19; // 该属性body为引用数据,拷贝的是地址,指向同一个真实数据,一个对象的该属性改变,另外一个对象的该属性也会随之改变
    console.log(obj1, "obj1");
    console.log(obj2, "obj2");

image.png

image.png

还有一种情况不要混淆!!!下面这个是直接修改了地址,所以两个对象之间也是不互相影响的。

    let obj1 = {
      name: "于家宝",
      body: {
        age: 18,
      },
    };
    // 拓展运算符是浅拷贝的一种方式
    let obj2 = { ...obj1 };
 
    obj1.name = "yujiabao"; // 该属性name为基本数据,复制的是真实数据,因此两个对象的该属性不会相互影响
    obj1.body = {
      age: 19,
    }; // 该属性body为引用数据,但是这样是直接改变了该属性的地址,因此两个对象的地址不一样,不指向同一个数据,所以两个对象的该属性不会相互影响
    console.log("obj1", obj1);
    console.log("obj2", obj2);

image.png

image.png

2. 深拷贝

创建一个对象,新对象和原始对象的属性完全相同,如果属性是基本数据类型,那么拷贝的就是基本数据类型存储在栈里的数据,两个对象的该属性之间不会相互影响,是独立的。如果属性是引用数据类型,那拷贝的也是数据,是栈里的地址指向堆内存里的数据,直接拷贝数据而不是像浅拷贝那样只拷贝地址,所以两个对象的该属性之间也是不会影响的,是独立的。

可以理解为递归的去拷贝了每一层的真实数据。

    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };
    // JSON.stringify是深拷贝的一种方式
    let obj2 = JSON.parse(JSON.stringify(obj1));
    obj1.name = "于家宝";
    obj1.body.age = 19; // 无论什么属性,新旧对象都不会互相产生影响
    console.log("obj1", obj1);
    console.log("obj2", obj2);

image.png

image.png

浅拷贝的实现方式

1. 拓展运算符

    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };
    let obj2 = { ...obj1 };
    obj1.name = "于家宝";
    obj1.body.age = 19;
    console.log(obj2.name); // yujiabao  不影响
    console.log(obj2.body.age); // 19  影响

2. Object.assign()

    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };
    let obj2 = Object.assign({}, obj1);
    obj1.name = "于家宝";
    obj1.body.age = 19;
    console.log(obj2.name); // yujiabao  不影响
    console.log(obj2.body.age); // 19  影响

3. Array.prototype.concat()

    let arr1 = [1, 2, [3, 4], 5];
    let arr2 = arr1.concat();
    arr1[0] = 6;
    arr1[2][0] = 7;
    console.log(arr2[0]); // 1 不影响
    console.log(arr2[2][0]); // 7 影响

4. Array.prototype.slice()

    let arr1 = [1, 2, [3, 4], 5];
    let arr2 = arr1.slice();
    arr1[0] = 6;
    arr1[2][0] = 7;
    console.log(arr2[0]); // 1 不影响
    console.log(arr2[2][0]); // 7 影响

5. Array.from()

    let arr1 = [1, 2, [3, 4], 5];
    let arr2 = Array.from(arr1);
    arr1[0] = 6;
    arr1[2][0] = 7;
    console.log(arr2[0]); // 1 不影响
    console.log(arr2[2][0]); // 7 影响

6. 手写实现浅拷贝

    function shallowCopy(params) {
      // 基本类型直接返回
      if (!params || typeof params !== "object") return params;

      // 根据 params 的类型判断是新建一个数组还是对象
      let newObject = Array.isArray(params) ? [] : {};

      // 遍历 params 并判断是 params 的属性才拷贝
      for (let key in params) {
        if (params.hasOwnProperty(key)) {
          newObject[key] = params[key];
        }
      }

      return newObject;
    }

    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };

    let obj2 = shallowCopy(obj1);
    obj1.name = '于家宝';
    obj1.body.age = 19;
    console.log(obj1); // { name: "于家宝", body: { age: 19 } }
    console.log(obj2); // { name: ""yujiabao"", body: { age: 19 } }

7. 函数库lodash提供的clone()方法

深拷贝的实现方式

1. JSON.parse(JSON.stringify())

    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };
    let obj2 = JSON.parse(JSON.stringify(obj1));
    obj1.name = "于家宝";
    obj1.body.age = 19;
    console.log(obj2.name); // yujiabao  不影响
    console.log(obj2.body.age); // 18  不影响

2. jQuery.extend()方法

    // 语法
    $.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝

    let $ = require('jquery');
    let obj1 = {
      name: "yujiabao",
      body: {
        age: 18,
      },
    };
    let obj2 = $.extend(true, {}, obj1);

3. 手写递归循环实现深拷贝

简单的:无法实现对函数,regExp等特殊对象的克隆,会抛弃对象的constructor,所有的构造函数都指向Object,如果有循环引用会报错。

    function deepCopy(object) {
      // 只拷贝对象
      if (!object || typeof object !== "object") return;
      // 根据 object 的类型判断是新建一个数组还是对象
      let newObj = Array.isArray(object) ? [] : {};
      // 遍历 object,并且判断是 object 的属性才拷贝
      for (let key in object) {
        if (object.hasOwnProperty(key)) {
          newObj[key] =
            typeof object[key] === "object"
              ? deepCopy(object[key])
              : object[key];
        }
      }
      return newObj;
    }

进阶版:

    function deepClone(target) {
      const map = new WeekMap();

      function isObject(target) {
        return (
          (typeof target === "object" && target) || typeof target === "function"
        );
      }

      function clone(data) {
        if (!isObject(target)) {
          return;
        }

        if ([Date, RegExp].includes(data.constructor)) {
          return new data.constructor(data);
        }

        if (typeof data === "function") {
          return new Function("return " + data.toString())();
        }

        const exist = map.get(data);
        if (exist) {
          return exist;
        }

        if (data instanceof Map) {
          const result = new Map();
          map.set(data, result);
          data.forEach((val, key) => {
            if (isObject(val)) {
              result.set(key, clone(val));
            } else {
              result.set(key, val);
            }
          });
          return result;
        }

        if (data instanceof Set) {
          const result = new Set();
          map.set(data, result);
          data.forEach((val) => {
            if (isObject(val)) {
              result.add(clone(val));
            } else {
              result.add(val);
            }
          });
          return result;
        }

        const keys = Reflect.ownKeys(data);
        const allDesc = Object.getOwnPropertyDescriptors(data);
        const result = Object.create(Object.getPrototypeOf(data), allDesc);
        map.set(data, result);
        keys.forEach((key) => {
          const val = data[key];
          if (isObject(val)) {
            result[key] = clone(val);
          } else {
            result[key] = val;
          }
        });
        return result;
      }

      return clone(target);
    }

4. structuredClone()

    let obj = { a: 1, b: { c: 2 }, d: [3, 4], e: new Date() };
    let deepCopy = structuredClone(obj);
 
    deepCopy.b.c = 100;
    deepCopy.d[0] = 300;
    console.log(obj.b.c); // 输出: 2
    console.log(obj.d[0]); // 输出: 3
    console.log(deepCopy.b.c); // 输出: 100
    console.log(deepCopy.d[0]); // 输出: 300
    console.log(deepCopy.e); // Thu Jan 02 2025 17:36:26 GMT+0800 (中国标准时间)

5. 函数库loadsh提供的cloneDeep()方法

浅拷贝和 = 赋值的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

举个例子:

    let obj1 = {
      name: "于家宝",
      body: {
        age: 18,
      },
    };
    let obj2 = Object.assign({}, obj1);
    obj1.name = "yujiabao";
    obj1.body.age = 19;
    console.log(obj2);

浅拷贝的话 基础类型name没有改变,引用类型body里面的age改变。

image.png

    let obj1 = {
      name: "于家宝",
      body: {
        age: 18,
      },
    };
    let obj2 = obj1;
    obj1.name = "yujiabao";
    obj1.body.age = 19;
    console.log(obj2);

赋值的话 基础类型name和引用类型body都改变了。

image.png