深拷贝、浅拷贝

92 阅读7分钟

深拷贝、浅拷贝

深拷贝和浅拷贝的区别

一、了解数据结构

1.基本数据类型(值类型)

  • number (数字)
  • string (字符串)
  • boolean (布尔值)
  • null (空)
  • undefined(未定义)
  • symbol (es6新增表示独一无二的值)

基本数据类型的特点:直接存储在栈(stack)中的数据

2.引用数据类型(对象类型)

  • Array (数组)
  • Function (函数)
  • Object (对象)
  • Date (日期)
  • Regexp (正则)

引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

二、了解堆栈

1.栈内存

  • 主要用于存放基本类型和对象变量的指针,算是一种简单的储存; 栈内存自动分配相对固定大小的内存空间,并由系统自动释放

2.堆内存

  • 主要用于存放引用类型,存储的对象类型数据对于大小在这方面都是未知的; 堆内存是动态分配内存,内存大小不一,也不会自动释放

三、如何判断数据类型

1.typeof

  • 在基本数据类型(null除外)和Function时,返回其对应类型;对于引用数据类型(Function除外)都返回为object;

    • 判断基本数据类型(null返回object,其余返回对应数据类型)

         const string = '321'; 
         const number = 321; 
         const boolean = true; 
         const enty = null; 
         const unassigned = undefined;
         const symbol = Symbol(); 
         const bigin = BigInt(1236587489845500); 
      
         console.log(typeof string) // string 
         console.log(typeof number) // number 
         console.log(typeof boolean) // boolean 
         console.log(typeof enty) // object 
         console.log(typeof unassigned) // undefined 
         console.log(typeof symbol) // symbol 
         console.log(typeof bigin) // bigin  
      
    • 判断引用数据类型(function 返回 function 其余均返回object;

         const arr = [1,2,3,4];
         const obj = {name: '1212'}; 
         const fun = () => {}; 
         const dates = new Date(); 
         const regexp = /[0-9]{1,2}/
      
         console.log(typeof arr) // object 
         console.log(typeof obj ) // object 
         console.log(typeof fun ) // function 
         console.log(typeof dates ) // object
         console.log(typeof regexp ) // object
      

2.instanceof

  • 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上; 基本数据类型 不存在prototype 因此不能使用 instanceof 来判断基本数据类型

      const arr = [1,2,3,4];
      const obj = {name: '1212'}; 
      const fun = () => {}; 
      const dates = new Date(); 
      const regexp = /[0-9]{1,2}/
    
      console.log(arr instanceof Array) // true  
      console.log(obj instanceof Object) // true 
      console.log(fun instanceof Function) // true 
      console.log(dates instanceof Date) // true
      console.log(regexp instanceof  RegExp) // true
    
    
      console.log(regexp instanceof  Date) // false
      console.log(regexp instanceof  Function) // false  
    

❗❗ 以上 使用instanceof 进行判断引用数据类型,实则是有问题的

  • instanceof 进行判断引用数据类型(存在的问题)
const arr = [1,2,3,4];
const obj = {name: '1212'}; 
const fun = () => {}; 
const dates = new Date(); 
const regexp = /[0-9]{1,2}/;

// 所有引用数据都是 Object 的实例,因此通过 instanceof 操作符检测任何引用数据和Object 构造函数都会返回 true
console.log(arr instanceof Object) // true  
console.log(obj instanceof Object) // true 
console.log(fun instanceof Object) // true 
console.log(dates instanceof Object) // true
console.log(regexp instanceof  Object) // true


console.log(regexp instanceof  Date) // false
console.log(regexp instanceof  Function) // false  
  • instanceof的原理:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上;

      // 其中 value 表示 instance 左边的数据, target表示 instance 右边边的数据
      function _instance(value, target) {
          let proto = value.__proto__; // 对象的原型
          let prototype = target.prototype; // 构造函数的 prototype 对象
    
          // 判断构造函数的 prototype 对象是否在对象的原型链上
          while (true) {
              if (!proto) return false;
              //TODO: 重点:当 proto 严格等于 prototype 时,返回 true
              if (proto === prototype) return true;
              proto = proto.__proto__;
          }
      }
    
      // 实验一下
      console.log(_instance([1,2,3], Array)); // true
      console.log(_instance([1,2,3], Function)); // false
    

3.constructor

  • 判断基本数据类型

      // constructor 判断基本数据类型
      const string = '321'; 
      const number = 321; 
      const boolean = true; 
      const enty = null; 
      const unassigned = undefined;
      const symbol = Symbol(); 
      const bigin = BigInt(1236587489845500);
      // null 与 undefined 不存在 原型
      console.log(string.constructor === String) // true 
      console.log(string.constructor === String) // true 
      console.log(number.constructor === Number ) // true 
      console.log(boolean.constructor === Boolean ) // true 
      console.log(symbol.constructor === Symbol) // true
      console.log(bigin.constructor === BigInt) // true  
    
  • 判断引用数据

    // constructor 判断引用数据类型
      const arr = [1,2,3,4];
      const obj = {name: '1212'}; 
      const fun = () => {}; 
      const dates = new Date(); 
      const regexp = /[0-9]{1,2}/
    
      console.log(arr.constructor === Array) // true 
      console.log(obj.constructor === Object ) // true 
      console.log(fun.constructor === Function ) // true 
      console.log(dates.constructor ===  Date) // true
      console.log(regexp.constructor === RegExp) // true  
    
  • 存在的问题

     function Functions() {}
     // 改变原型
     Functions.prototype = new Array();
     let tempF = new Functions();
     console.log(tempF.constructor === Functions); // false
     console.log(tempF.constructor === Array); // true  
    

如果对象改变了原型,那么使用constructor不再准确;

4.Object.prototype.toString

  • 判断基本数据

      // 由于返回格式“[object Xxx]”的字符串,需要对Object.prototype.toString进行处理
      // 检测数据类型功能函数
      const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
    
      // 判断基本数据类型
      const string = '321'; 
      const number = 321; 
      const boolean = true; 
      const enty = null; 
      const unassigned = undefined;
      const symbol = Symbol(); 
      const bigin = BigInt(1236587489845500);
    
      console.log(checkedType(string)); // string
      console.log(checkedType(number)); // number
      console.log(checkedType(boolean)); // boolean
      console.log(checkedType(enty)); // null
      console.log(checkedType(unassigned)); // undefined
      console.log(checkedType(symbol)); // symbol
      console.log(checkedType(bigin)); // bigin  
    
  • 判断引用数据

     // 由于返回格式“[object Xxx]”的字符串,需要对Object.prototype.toString进行处理
     // 检测数据类型功能函数
     const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
    
     // 判断引用数据类型
     const arr = [1,2,3,4];
     const obj = {name: '1212'}; 
     const fun = () => {}; 
     const dates = new Date(); 
     const regexp = /[0-9]{1,2}/
    
     console.log(checkedType(arr)); // array
     console.log(checkedType(obj)); // object
     console.log(checkedType(fun)); // function
     console.log(checkedType(dates)); // date
     console.log(checkedType(regexp)); // regexp  
    

四、深拷贝浅拷贝

1.深拷贝

  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)

2.浅拷贝

  • 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象(只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃)

3.浅拷贝与赋值的区别

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的

  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

      // 对象赋值
      let obj1 = {
          name: 'Chen',
          age: 18,
          hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
      }
    
      let obj2 = obj1;
      obj2.name = 'Forever';
      obj2.hobby[1] = 'swim';
      obj2.hobby[2] = 'alpinism';
      console.log('obj1===>', obj1);
      console.log('obj2===>', obj2);  
      
      // 浅拷贝
      let obj1 = {
          name: 'Chen',
          age: 18,
          hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
      }
    
      let obj3 = {...obj1};
      obj3.name = 'Forever';
      obj3.hobby[1] = 'swim';
      obj3.hobby[2] = 'alpinism';
      console.log('obj1===>', obj1);
      console.log('obj3===>', obj3);  
    

4.浅拷贝的实现

  • 展开运算符

      // 展开运算符... 实现浅拷贝
      let obj1 = {
          name: 'Chen',
          hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
      }
    
      let obj2 = {...obj1};
      obj2.hobby[1] = 'swim';
      obj2.hobby[2] = 'alpinism';
      obj2.name = 'Forever';
      console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}
      console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}  
    
  • object.assign

      // Object.assign() 实现浅拷贝
      let obj1 = {
        name: "Chen",
        hobby: ["see a film", "write the code", "play basketball", "tourism"],
      };
    
      let obj2 = Object.assign({}, obj1);
      obj2.hobby[1] = "swim";
      obj2.hobby[2] = "alpinism";
      obj2.name = "Forever";
      console.log("obj1===>", obj1); // obj1===> {name: 'Chen',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}
      console.log("obj2===>", obj2); // obj2===> {name: 'Forever',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}  
    

当object只有一层的时候,是深拷贝;所以当原数据进行浅拷贝,改变obj2的name 原数据obj1中的name不会改变;

  • Array.prototype.concat

      // Array.prototype.concat() 实现浅拷贝
      let arr1 =  [
          {
              name: 'Chen'
          },
          'see a film', 
          'write the code', 
          'play basketball', 
          'tourism'
      ];
      let arr2 = arr1.concat([]);
      arr2[0].name = 'Forever';
      arr2[1] = 'play games';
      console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
      console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']  
    
  • Array.prototype.slice

      // Array.prototype.concat() 实现浅拷贝
      let arr1 =  [
          {
              name: 'Chen'
          },
          'see a film', 
          'write the code', 
          'play basketball', 
          'tourism'
      ];
      let arr2 = arr1.slice();
      arr2[0].name = 'Forever';
      arr2[1] = 'play games';
      console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
      console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']  
    

5.深拷贝的实现

  • JSON.parse(JSON.stringify())

      //  JSON.parse(JSON.stringify())实现深拷贝Object
      let obj1 = {
          name: 'Chen',
          hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
      }
      let obj2 = JSON.parse(JSON.stringify(obj1));
      console.log(obj1 === obj2); // false
      obj2.name = 'Forever';
      obj2.hobby[1] = 'swim';
      obj2.hobby[2] = 'alpinism';
    
      console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: ['see a film', 'write the code', 'play basketball', 'tourism']}
      console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: ['see a film', 'swim', 'alpinism', 'tourism']}  
      
      //  JSON.parse(JSON.stringify())实现深拷贝Array
      let arr1 =  [
          {
              name: 'Chen'
          },
          'see a film', 
          'write the code', 
          'play basketball', 
          'tourism'
      ];
      let arr2 = JSON.parse(JSON.stringify(arr1));
      console.log(arr1 === arr2); // false
      arr2[0].name = 'Forever';
      arr2[1] = 'play games';
      console.log('arr1===>', arr1); // arr1===> [{ name: 'Chen' },'see a film','write the code','play basketball', 'tourism']
      console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']  
    
  • 递归实现

        function cloneDeepDi(obj){
        const newObj = {};
        let keys = Object.keys(obj);
        let key = null;
        let data = null;
        for(let i = 0; i<keys.length;i++){
        key = keys[i];
        data = obj[key];
        if(data && typeof data === 'object'){
        newObj[key] = cloneDeepDi(data)
        }else{
        newObj[key] = data;
        }
        }
        return newObj
        }  
    
    • 解决循环引用的递归问题

          function deepCopy(obj, parent = null) {
          // 创建一个新对象
          let result = {};
          let keys = Object.keys(obj),
          key = null,
          temp = null,
          _parent = parent;
          // 该字段有父级则需要追溯该字段的父级
          while (_parent) {
          // 如果该字段引用了它的父级则为循环引用
          if (_parent.originalParent === obj) {
          // 循环引用直接返回同级的新对象
          return _parent.currentParent;
          }
          _parent = _parent.parent;
          }
          for (let i = 0; i < keys.length; i++) {
          key = keys[i];
          temp = obj[key];
          // 如果字段的值也是一个对象
          if (temp && typeof temp === 'object') {
          // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
          result[key] = DeepCopy(temp, {
          originalParent: obj,
          currentParent: result,
          parent: parent
          });
          } <span class="hljs-keyword">else</span> {
          result[key] = temp;
          }
          } <span class="hljs-keyword">return</span> result;
      
          }  
      
  • lodash的_.clonedeep

      var _ = require('lodash');
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = _.cloneDeep(obj1);
      console.log(obj1.b.f === obj2.b.f);// false