浅析JavaScript之深浅拷贝

923 阅读5分钟

449626073188794531.jpg

首先我们要明白JS的数据类型分为两种,一种是基本数据类型,另一种是引用数据类型。

基本数据类型有6种:undefinednullnumberbooleanstringsymbol(ES6)。

引用数据类型:objectArray,Function

两者主要区别在于:

基本数据类型存储的是值,引用数据类型中存储的是地址。当创建一个引用类型的时候,计算机会在内存中帮我们开辟一个空间存放,这个空间有一个地址。且引用数据类型的值是保存在栈内存和堆内存中的对象。栈区内存保存变量标识符和指向堆内存中该对象的指针。当寻找引用值时,解释器会先寻找栈中的地址。然后根据地址找到堆内存的实体。

而大多数实际项目中,我们想要的结果是两个变量(初始值相同)互不影响。所以就要使用到拷贝(分为深浅两种)

浅拷贝

对一个对象进行复制生成新的对象,新的对象要开辟一块新的内存来存储,新对象中的基本类型属性和String类型属性都会开辟新的空间存储,但是如果是引用类型的属性,那这个引用类型的属性还是指向原对象的引用属性内存,当对新的对象或原对象的引用属性做出改变的时候,两方的引用属性类型的值同时做出改变。

浅拷贝的方法有很多:object.assing,拓展运算符方法,concat拷贝数组,slice拷贝数组等。

我们看看拓展运算符方式的浅拷贝

/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果

扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

手工实现一个浅拷贝

根据上述理解,如何手工实现一个浅拷贝。

  • 对基础类型做一个最基本的一个拷贝;

  • 对引用类型开辟一个新的存储,并且拷贝一层对象属性 请看如下代码:

        var Obj = { 
            func: function () { alert(1) },
            obj: {a:1,b:{c:2}},
            arr: [1,2,3],
            und: undefined, 
            reg: /123/,
            date: new Date(0), 
            NaN: NaN,
            infinity: Infinity,
            sym: Symbol(1)
      }
  const shallowClone = (target) => {
    if (typeof target === 'object' && target !== null) {
      const cloneTarget = Array.isArray(target) ? []: {};
      for (let prop in target) {
        if (target.hasOwnProperty(prop)) {
            cloneTarget[prop] = target[prop];
        }
      }
      return cloneTarget;
    } else {
      return target;
    }
  }
  shallowClone(Obj)

从该代码可以看出,利用判断类型,针对引用类型的对象进行for循环遍历对象属性赋值给目标对象的属性,基本就手工实现一个浅拷贝的代码了。

深拷贝

创建一个新对象,将原对象的各个属性的值拷贝过来。深拷贝要把复制对象所引用的对象都复制一遍。

方法一:乞丐版(JSON.stringfy

SON.stringfy() 其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。

let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

这里需要注意的点

  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)

方法二:手写递归实现(基础版)

let obj1 = {
  a:{
    b:1
  }
}
function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍历
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归
    } else {
      cloneObj[key] = obj[key]  //基本类型的话直接复制值
    }
  }
  return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

这种基础版本的写法也比较简单,可以应对大部分的应用情况。但还是有一定的缺陷:这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝。 来看看改进版

方法三:改进版(改进后递归实现)

    //需要拷贝的对象
    var obj = {
        num: 0,
        str: '',
        boolean: true,
        unf: undefined,
        nul: null,
        obj: { name: '对象', id: 1, gender: 1  },
        arr: [0, 1, 2],
        func: function () { console.log('函数') },
        date: new Date(0),
        reg: new RegExp('/正则/ig'),
        [Symbol('1')]: 1,
      };
      Object.defineProperty(obj, 'innumerable', {
        enumerable: false, value: '不可枚举属性' }
      );
      obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
      obj.loop = obj    // 设置loop成循环引用的属性
      //判断数据类型
      function ifType(val){
        let type  = typeof val;
        if (type !== "object") {
          return type;
        }
        return Object.prototype.toString.call(val).replace(/^\[object (\S+)\]$/, '$1');
      }
      //拷贝代码
      const deepClone = function (obj, hash = new WeakMap()) {
        if (ifType(obj) === 'Date') 
        return new Date(obj)       // 日期对象直接返回一个新的日期对象
        if (ifType(obj) === 'RegExp')
        return new RegExp(obj)     //正则对象直接返回一个新的正则对象
        //如果循环引用了就用 weakMap 来解决
        if (hash.has(obj)) return hash.get(obj)
        let allDesc = Object.getOwnPropertyDescriptors(obj)
        //遍历传入参数所有键的特性
        let copyObj = Object.create(Object.getPrototypeOf(obj), allDesc)
        //继承原型链
        hash.set(obj, copyObj)
        const isType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
        for (let key of Reflect.ownKeys(obj)) { 
          copyObj[key] = (isType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
        }
        return copyObj
      }
   //验证
  let copyObj = deepClone(obj)
  copyObj.arr.push(4)
  console.log('obj', obj)
  console.log('cloneObj', copyObj)

控制台结果:

image.png

image.png

总结

在JavaScript当中,深浅拷贝的知识是比较重要的一环,这对于你深入了解JS底层的原理有很大帮助,对于任何问题,应该用类似的方法去分析每个问题深入考察的究竟是什么,这样才能更好地去全面提升自己的基本功。