完美深拷贝 | 青训营笔记

109 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第4天

如果只想看完美深拷贝☞点击这里,快速导航

JS深拷贝与浅拷贝

参考文章1

参考文章2

引入案例

拷贝:数据复制

 let num1 = 1;
 let num2 = num1;
 console.log(num1);
 console.log(num2);
 let arr = [1, 2, 3, 4];
 let Arr = arr;//这里复制的是指针
 Arr[0] = 66;
 console.log("原来的数组" + arr);
 console.log("拷贝的数组" + Arr);

原因

copy.png

解决方法

数组:slice()和concat()方法,扩展运算符,遍历
 let arr = [1, 2, 3, 4];
 //let Arr=[];
 //arr.forEach(function(element){
 //    Arr.push(element);
 //})
 //let Arr = arr.slice();
 //let Arr = arr.concat();
 //let Arr = [...arr];
 Arr[0] = 66;
 console.log("原来的数组" + arr);
 console.log("拷贝的数组" + Arr);
对象:Object.assign(),遍历
 Object.assign(target, ...sources)

参数:

target:目标对象。 sources:任意多个源对象。 返回值:目标对象会被返回

 var obj1 = {
     a: "hello",
     b: {
         a: "hello",
         b: 21}
 };
 var cloneObj1= Object.assign({}, obj1);
 cloneObj1.a = "changed";
 //cloneObj1.b.a = "changed";
 console.log(obj1.a);  //hello
 //console.log(obj1.b.a); // "changed"

调用函数遍历

 function simpleClone(initalObj) {
     var obj = {};
     for ( var i in initalObj) {
         obj[i] = initalObj[i];
     }
     return obj;
 }

以上做法仅实现一级拷贝

 let arr = [1, [1, 2, 3], {
     a: 11,
     b: 22,
     c: {
         c1: 11,
         c2: 12
     }
 }];
 let Arr=[];
 arr.forEach(function(element){
     Arr.push(element);
 })
 let Arr = arr.slice();
 let Arr = arr.concat();
 let Arr = [...arr];
 Arr[0] = 66;
 Arr[1][0] = 66;
 Arr[2].a = 66;
 Arr[2].c.c1 = 66;
 console.log(arr);
 console.log(Arr);

深拷贝

如果目标数组或对象只有一层,可以采用以上方法实现深拷贝。

我自己的理解:拷贝之后,如果变量里存的是一个引用值的地址,就是浅拷贝;反之,存的是一个原始值或者函数,就是深拷贝

实现方法
1.jQuery的extend方法

语法:$.extend( [deep ], target, object ) ①deep表示是否深拷贝,为true为深拷贝,为false为浅拷贝 ②target目标对象,其他对象的成员属性将被附加到该对象 ③object拷贝源对象

 let arr = [1, [1, 2, 3], {
     a: 11,
     b: 22,
     c: {
         c1: 11,
         c2: 12
     }
 }];
 let Arr = $.extend(true,[],arr);
 Arr[0] = 66;
 Arr[1][0] = 66;
 Arr[2].a = 66;
 Arr[2].c.c1 = 66;
 console.log(arr);
 console.log(Arr);
2.JSON的序列化和反序列化

JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。

 JSON.parse(JSON.stringify(XXXX))
 let arr = [1, [1, 2, 3], {
     a: 11,
     b: 22,
     c: {
         c1: 11,
         c2: 12
     }
 }];
 let Arr = JSON.parse(JSON.stringify(arr));
 Arr[0] = 66;
 Arr[1][0] = 66;
 Arr[2].a = 66;
 Arr[2].c.c1 = 66;
 console.log(arr);
 console.log(Arr);

缺点:

  1. 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。
  2. Date对象, RegExp对象, Error对象等是无法通过这种方式深拷贝。这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。
  3. 如果原对象中有值为undefined的情况, JSON.stringify 后会丢失
  4. 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
3.递归拷贝
 function deepClone(initalObj, finalObj) {    
   var obj = finalObj || {};    
   for (var i in initalObj) {        
     var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
     if(prop === obj) {            
       continue;
     }        
     if (typeof prop === 'object') {
       obj[i] = (prop.constructor === Array) ? [] : {};            
       arguments.callee(prop, obj[i]);//调用自身
     } else {
       obj[i] = prop;
     }
   }    
   return obj;
 }
 var str = {};
 var obj = { a: {a: "hello", b: 21} };
 deepClone(obj, str);
 console.log(str.a);
4.使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。

 function deepClone(initalObj, finalObj) {    
   var obj = finalObj || {};    
   for (var i in initalObj) {        
     var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
     if(prop === obj) {            
       continue;
     }        
     if (typeof prop === 'object') {
       obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
     } else {
       obj[i] = prop;
     }
   }    
   return obj;
 }

👆 2022.08.21

👇 2022.10.22

完美版深拷贝

评价一个深拷贝是否完善,请检查以下问题是否都实现了:

  • 基本类型数据是否能拷贝?
  • 键和值都是基本类型的普通对象是否能拷贝?
  • Symbol作为对象的key是否能拷贝?
  • Date和RegExp对象类型是否能拷贝?
  • Map和Set对象类型是否能拷贝?
  • Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
  • 对象的原型是否能拷贝?
  • 不可枚举属性是否能拷贝?
  • 循环引用是否能拷贝?

测试数据

const obj = {
  // =========== 1.基础数据类型 ===========
  num: 0, // number
  str: '', // string
  bool: true, // boolean
  unf: undefined, // undefined
  nul: null, // null
  sym: Symbol('sym'), // symbol
  bign: BigInt(1n), // bigint

  // =========== 2.Object类型 ===========
  // 普通对象
  obj: {
    name: '我是一个对象',
    id: 1
  },
  // 数组
  arr: [0, 1, 2],
  // 函数
  func: function () {
    console.log('我是一个函数')
  },
  // 日期
  date: new Date(0),
  // 正则
  reg: new RegExp('/我是一个正则/ig'),
  // Map
  map: new Map().set('mapKey', 1),
  // Set
  set: new Set().add('set'),
  // =========== 3.其他 ===========
  [Symbol('1')]: 1  // Symbol作为key
};

// 4.添加不可枚举属性
Object.defineProperty(obj, 'innumerable', {
  enumerable: false,
  value: '不可枚举属性'
});

// 5.设置原型对象
Object.setPrototypeOf(obj, {
  proto: 'proto'
})

// 6.设置loop成循环引用的属性
obj.loop = obj

直接上代码

function deepClone(source) {
  // 采用WeakMap 构建 Hash表处理循环引用,WeakMap 有利于垃圾回收
  const cache = new WeakMap()
  // 策略模式缓存每种对象数据类型的处理方式 返回一个函数
  let targetResult = {
    Function: function (source) {
      return new Function('return ' + source.toString())()
    },
    // 日期或者正则对象则直接构造一个新的对象返回
    Date: function (source) {
      return new Date(source)
    },
    RegExp: function (source) {
      return new RegExp(source)
    },
    // Map 和 Set new 一个新的实例 注意内部存的值也需要进行递归深拷贝
    Map: function (source) {
      let result = new Map()
      cache.set(source, result) // 设置缓存要在递归之前,才能处理循环引用
      source.forEach((val, key) => {
        result.set(key, clone(val))
      })
      return result
    },
    Set: function (source) {
      let result = new Set()
      cache.set(source, result) // 设置缓存要在递归之前,才能处理循环引用
      source.forEach((val, key) => {
        result.add(clone(val))
      })
      return result
    },
    Array: function (source) {
      let result = []
      cache.set(source, result) // 设置缓存要在递归之前,才能处理循环引用
      source.forEach(val => {
        result.push(clone(val))
      })
      return result
    },
    Object: function (source) {
      // Reflect.ownKeys能够遍历对象的不可枚举属性以及 Symbol 类型,
      const keys = Reflect.ownKeys(source)
      // Object.getOwnPropertyDescriptors()设置属性描述对象
      const allDesc = Object.getOwnPropertyDescriptors(source)
      // Object.create()方式继承原型链
      const result = Object.create(Object.getPrototypeOf(source), allDesc)
      cache.set(source, result) // 设置缓存要在递归之前,才能处理循环引用
      // Object.create()是浅拷贝 递归执行实现深拷贝
      keys.forEach(key => {
        result[key] = clone(source[key])
      })
      return result
    },
  }

  function clone(source) {
    // 基础数据类型 直接返回 包括 null
    if (source instanceof Object === false) return source
    // 如果有缓存,就从缓存中取
    if (cache.has(source)) return cache.get(source)
    // [object Object] 截取第9个到倒数第1个字符 Object
    let sourceType = Object.prototype.toString.call(source).slice(8, -1)
    // 拿到拷贝完成的结果
    let result = targetResult[sourceType](source)
    return result
  }

  return clone(source)
}