如何实现一个深浅拷贝?

80 阅读4分钟

浅拷贝的定义

自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

方法一:object.assign

let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b: 10 } }; 
source.a.b = 10; 
console.log(source); // { a: { b: 10 } }; 
console.log(target); // { a: { b: 10 } };

从上面代码中我们可以看到,首先通过 Object.assign 将 source 拷贝到 target 对象中,然后我们尝试将 source 对象中的 b 属性由 2 修改为 10。通过控制台可以发现,打印结果中,三个 target 里的 b 属性都变为 10 了,证明 Object.assign 暂时实现了我们想要的拷贝效果。

但是使用 object.assign 方法有几点需要注意:

  • 它不会拷贝对象的继承属性;

  • 它不会拷贝对象的不可枚举的属性;

  • 可以拷贝 Symbol 类型的属性。

可以简单理解为:Object.assign 循环遍历原对象的属性,通过复制的方式将其赋值给目标对象的相应属性。

方法二:扩展运算符方式

/* 对象的拷贝 */
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 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

方法三:concat 拷贝数组

数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]

方法四:slice 拷贝数组

slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]

深拷贝的原理和实现

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。

方法一:乞丐版(JSON.stringify)

JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 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]}

但是使用 JSON.stringify 实现深拷贝还是有一些地方值得注意

  1. 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;

  2. 拷贝 Date 引用类型会变成字符串;

  3. 无法拷贝不可枚举的属性;

  4. 无法拷贝对象的原型链;

  5. 拷贝 RegExp 引用类型会变成空对象;

  6. 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

  7. 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

    function Obj() { 
      this.func = function () { alert(1) }; 
      this.obj = {a:1};
      this.arr = [1,2,3];
      this.und = undefined; 
      this.reg = /123/; 
      this.date = new Date(0); 
      this.NaN = NaN;
      this.infinity = Infinity;
      this.sym = Symbol(1);
    } 
    let obj1 = new Obj();
    Object.defineProperty(obj1,'innumerable',{ 
      enumerable:false,
      value:'innumerable'
    });
    console.log('obj1',obj1);
    let str = JSON.stringify(obj1);
    let obj2 = JSON.parse(str);
    console.log('obj2',obj2);
    

手写深拷贝

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}}