JS深拷贝和浅拷贝的实现

作者: puxiaotaoc | 来源:发表于2018-08-12 19:11 被阅读111次

    一、JS的基本数据类型

    • 基本数据类型:String,Boolean,Number,Undefined,Null;
    • 引用数据类型:Object(Array,Date,RegExp,Function);
    • 基本数据类型和引用数据类型的区别:
      1、保存位置不同:基本数据类型保存在栈内存中,引用数据类型保存在堆内存中,然后在栈内存中保存了一个对堆内存中实际对象的引用,即数据在堆内存中的地址,JS对引用数据类型的操作都是操作对象的引用而不是实际的对象,如果obj1拷贝了obj2,那么这两个引用数据类型就指向了同一个堆内存对象,具体操作是obj1将栈内存的引用地址复制了一份给obj2,因而它们共同指向了一个堆内存对象;
      为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?
      1)堆比栈大,栈比堆速度快;
      2)基本数据类型比较稳定,而且相对来说占用的内存小;
      3)引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
      4)堆内存是无序存储,可以根据引用直接获取;

    按引用访问:js不允许直接访问保存在堆内存中的对象,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值;

    ECMAScript中所有函数的参数都是按值来传递的,对于原始值,只是把变量里的值传递给参数,之后参数和这个变量互不影响,对于引用值,对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因,因为它们都指向同一个对象;
    2、基本数据类型使用typeof可以返回其基本数据类型,但是NULL类型会返回object,因此null值表示一个空对象指针;引用数据类型使用typeof会返回object,此时需要使用instanceof来检测引用数据类型;
    3、定义引用数据类型需要使用new操作符,后面再跟一个构造函数来创建;
    1)使用new操作符创建对象;

    var obj1 = new Object();
    obj1.a = 1;
    

    2)使用对象字面量表示法创建对象;

    var obj1 = {
      a: 1,
      b: 2
    }
    

    3)可以通过点表示法访问对象的属性,也可以使用方括号表示法来访问对象的属性;

    • ES6新增数据类型:Map,Set,Generator,Symbol
    • 本地对象:ECMA-262 把本地对象(native object)定义为“独立于宿主环境的 ECMAScript 实现提供的对象”,即本地对象就是 ECMA-262 定义的类(引用类型);
    • 宿主对象:宿主”就是我们网页的运行环境,即“操作系统”和“浏览器”,所有非本地对象都是宿主对象(host object),即由 ECMAScript 实现的宿主环境提供的对象,所有的BOM和DOM对象都是宿主对象,因为其对于不同的“宿主”环境所展示的内容不同,即ECMAScript官方未定义的对象都属于宿主对象,因为其未定义的对象大多数是自己通过ECMAScript程序创建的对象;
    • JS内置对象:是指JS语言自带的一些对象,供开发者使用,这些对象提供了一些常用的或是最基本而必要的功能;
      1、Arguments:函数参数集合;
      2、Array对象:length,instanceof,isArray(),toString()返回字符串,valueOf()返回数组的值,join()可以将数组转为字符串,push(),pop(),shift(),unshift(),reverse(),sort(),
      slice(),splice(),indexOf(),lastIndexOf(),迭every(),filter(),forEach(),map(),some(),
      归并方法reduce(),reduceRight();
      3、Boolean:布尔对象;
      4、Error:异常对象;
      5、Number:数值对象;
      6、String对象:length,charAt()返回指定位置的字符,concat(),slice(),subString(),
      subStr(),indexOf(),lastIndexOf(),trim(),toLowerCase(),toUpperCase(),split(),
      text.match(),text.splice();
      7、Date对象:toUTCstring(),getTime();
      8、RegExp对象:test();
      9、Function对象:arguments,this,apply(this,arguments),call(this,num1,num2);
      10、Math对象:min(),max(),ceil(),floor(),round(),random();
      11、Global对象:encodeURI,encodeURIComponent,parseInt(),eval();
      12、Object对象:prototype,constructor;
    • 基本包装类型:Boolean,Number,String
      1、转换为数值:parseInt()专门用于把字符串转换成数值,Number()用于任何类型;
      2、非字符转换成字符:toString();
      3、数组转成字符:join();
      4、字符串转换成数组:split();
    // 基本数据类型的复制,基本数据类型是按值传递的
    var a = 1;
    var b = a;
    b = 2;
    console.log(a);  // 1
    console.log(b);  // 2
    
    // 引用数据类型的复制,引用数据类型按引用传值
    var obj1 = {
      a: 1,
      b: 2
    }
    var obj2 = obj1;
    obj2.a = 3;
    console.log(obj1.a); // 3
    console.log(obj2.a); // 3
    

    二、JS浅拷贝

    深拷贝和浅拷贝
    • 深拷贝和浅拷贝简单解释
           浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
           区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制;
    // 只复制第一层的浅拷贝
    function simpleCopy(obj1) {
       var obj2 = Array.isArray(obj1) ? [] : {};
       for (let i in obj1) {
       obj2[i] = obj1[i];
      }
       return obj2;
    }
    var obj1 = {
       a: 1,
       b: 2,
       c: {
       d: 3
      }
    }
    var obj2 = simpleCopy(obj1);
    obj2.a = 3;
    obj2.c.d = 4;
    alert(obj1.a); // 1
    alert(obj2.a); // 3
    alert(obj1.c.d); // 4
    alert(obj2.c.d); // 4
    
    • Object.assign()实现浅拷贝及一层的深拷贝
    let obj1 = {
       a: {
         b: 1
       },
       c: 2
    }
    let obj2 = Object.assign({},obj1)
    obj2.a.b = 3;
    obj2.c = 3
    console.log(obj1.a.b); // 3
    console.log(obj2.a.b); // 3
    console.log(obj1.c); // 2
    console.log(obj2.c); // 3
    

    二、JS深拷贝

    • 手动实现深拷贝
    let obj1 = {
       a: 1,
       b: 2
    }
    let obj2 = {
       a: obj1.a,
       b: obj1.b
    }
    obj2.a = 3;
    alert(obj1.a); // 1
    alert(obj2.a); // 3
    
    let obj1 = {
       a: {
         b: 2
       }
    }
    let obj2 = {
       a: obj1.a
    }
    obj2.a.b = 3;
    console.log(obj1.a.b); // 3
    console.log(obj2.a.b); // 3
    
    • 递归实现深拷贝
    function deepCopy(obj1) {
          var obj2 = Array.isArray(obj1) ? [] : {};
          if (obj1 && typeof obj1 === "object") {
            for (var i in obj1) {
              if (obj1.hasOwnProperty(i)) {
                // 如果子属性为引用数据类型,递归复制
                if (obj1[i] && typeof obj1[i] === "object") {
                  obj2[i] = deepCopy(obj1[i]);
                } else {
                  // 如果是基本数据类型,只是简单的复制
                  obj2[i] = obj1[i];
                }
              }
            }
          }
          return obj2;
        }
        var obj1 = {
          a: 1,
          b: 2,
          c: {
            d: 3
          }
        }
        var obj2 = deepCopy(obj1);
        obj2.a = 3;
        obj2.c.d = 4;
        alert(obj1.a); // 1
        alert(obj2.a); // 3
        alert(obj1.c.d); // 3
        alert(obj2.c.d); // 4
    

    缺陷:当遇到两个互相引用的对象,会出现死循环的情况,为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环;

    function deepCopy(obj1) {
          var obj2 = Array.isArray(obj1) ? [] : {};
          if (obj1 && typeof obj1 === "object") {
            for (var i in obj1) {
              var prop = obj1[i]; // 避免相互引用造成死循环,如obj1.a=obj
              if (prop == obj1) {
                continue;
              }
              if (obj1.hasOwnProperty(i)) {
                // 如果子属性为引用数据类型,递归复制
                if (prop && typeof prop === "object") {
                  obj2[i] = (prop.constructor === Array) ? [] : {};
                  arguments.callee(prop, obj2[i]); // 递归调用
                } else {
                  // 如果是基本数据类型,只是简单的复制
                  obj2[i] = prop;
                }
              }
            }
          }
          return obj2;
        }
        var obj1 = {
          a: 1,
          b: 2,
          c: {
            d: 3
          }
        }
        var obj2 = deepCopy(obj1);
        obj2.a = 3;
        obj2.c.d = 4;
        alert(obj1.a); // 1
        alert(obj2.a); // 3
        alert(obj1.c.d); // 3
        alert(obj2.c.d); // 4
    
    // Object.create实现深拷贝1,但也只能拷贝一层
    function deepCopy(obj1) {
          var obj2 = Array.isArray(obj1) ? [] : {};
          if (obj1 && typeof obj1 === "object") {
            for (var i in obj1) {
              var prop = obj1[i]; // 避免相互引用造成死循环,如obj1.a=obj
              if (prop == obj1) {
                continue;
              }
              if (obj1.hasOwnProperty(i)) {
                // 如果子属性为引用数据类型,递归复制
                if (prop && typeof prop === "object") {
                  obj2[i] = (prop.constructor === Array) ? [] : Object.create(prop);
                } else {
                  // 如果是基本数据类型,只是简单的复制
                  obj2[i] = prop;
                }
              }
            }
          }
          return obj2;
        }
        var obj1 = {
          a: 1,
          b: 2,
          c: {
            d: 3
          }
        }
        var obj2 = deepCopy(obj1);
        obj2.a = 3;
        obj2.c.d = 4;
        alert(obj1.a); // 1
        alert(obj2.a); // 3
        alert(obj1.c.d); // 3
        alert(obj2.c.d); // 4
    
    // Object实现拷贝2,浅拷贝
    var obj1 = {
          a: 1,
          b: 2,
          c: {
            d: 3
          }
        }
        var obj2 = Object.create(obj1);
        obj2.a = 3;
        obj2.c.d = 4;
        alert(obj1.a); // 1
        alert(obj2.a); // 3
        alert(obj1.c.d); // 4
        alert(obj2.c.d); // 4
    
    • 使用JSON.stringify和JSON.parse实现深拷贝:JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象;
    function deepCopy(obj1){
        let _obj = JSON.stringify(obj1);
        let obj2 = JSON.parse(_obj);
        return obj2;
      }
        var a = [1, [1, 2], 3, 4];
        var b = deepCopy(a);
        b[1][0] = 2;
        alert(a); // 1,1,2,3,4
        alert(b); // 2,2,2,3,4
    

    缺陷:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON;

    let obj1 = {
       fun:function(){
          alert(123);
       }
    }
    let obj2 = JSON.parse(JSON.stringify(obj1));
    console.log(typeof obj1.fun); // function
    console.log(typeof obj2.fun); // undefined
    
    • 热门的函数库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
    
    • jquery实现深拷贝
      jquery 提供一个$.extend可以用来做深拷贝;
    var $ = require('jquery');
    var obj1 = {
       a: 1,
       b: {
         f: {
           g: 1
         }
       },
       c: [1, 2, 3]
    };
    var obj2 = $.extend(true, {}, obj1);
    console.log(obj1.b.f === obj2.b.f);  // false
    
    • slice是否为深拷贝
    // 对只有一级属性值的数组对象使用slice
    var a = [1,2,3,4];
    var b = a.slice();
    b[0] = 2;
    alert(a); // 1,2,3,4
    alert(b); // 2,2,3,4
    
    // 对有多层属性的数组对象使用slice
    var a = [1,[1,2],3,4];
    var b = a.slice();
    b[1][0] = 2;
    alert(a); // 1,2,2,3,4
    alert(b); // 1,2,2,3,4
    

    结论:slice()和concat()都并非深拷贝;

    相关文章

      网友评论

      本文标题:JS深拷贝和浅拷贝的实现

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