对于深拷贝和浅拷贝的理解

对于深拷贝和浅拷贝的理解

深拷贝和浅拷贝经常出现在许多前端的面试题当中,同时在写代码的时候,经常会遇到修改一个对象属性的时候,跟它关联的对象也会发生变化等莫名其妙的问题,通过深入理解这些原理,能有助于我们更好的编程。

对于经常使用 Mac 系统的同学来说,拷贝一词并不陌生,说白了就是复制么。那么深复制和浅复制到底复制的是什么东西呢?对于 JavaScript 来说,其实就是在复制数据类型。我们都知道在 JavaScript 中,数据类型大致可以分为两类,一类是基本数据类型,一类是复杂数据类型。对于基本数据类型,不存在浅拷贝。对于复杂类型(这里指的是对象和数组),不同的复制有着不同的结果。

浅拷贝(浅复制)

举一个栗子:

let arr1 = [1, 2, 3];
let arr2 = arr1;
arr1[0] = 4;
console.log(arr1); // [4, 2, 3]
console.log(arr2); // [4, 2, 3]
arr2[0] = 5;
console.log(arr1); // [5, 2, 3]
console.log(arr2); // [5, 2, 3]

在举一个栗子:

let obj1 = { name: '张三' };
let obj2 = obj1;
obj1.name = '李四';
console.log(obj1.name); // 李四
console.log(obj2.name); // 李四
obj2.name = '王五';
console.log(obj1.name); // 王五
console.log(obj2.name); // 王五

眼力尖的同学一下子就会看出问题的所在了,不管是我修改源数据还是赋值后的数据,只要是有上述赋值关系,它们之间就会存在某种联系,导致相互修改会受到影响。重点来了,对于源数据和赋值后的数据,它们共享了同一块内存的引用地址。也就是说,共同的数据,不同的引用变量,修改哪一个变量都会更新它们共同的数据,从而另外一个变量也会变化。如果用客户端-服务器的方式阐述的话,多个客户端指向同一台服务器,当服务器上面的资源发生改变后,客户端收到的资源同样都是改变后的。

除过上面这种简单的赋值方式外,还有一些方法也可以达到浅拷贝的目的。

Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象。

let obj1 = { a: 1, b: 2, c: { d: 3 } };
let obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2, c: { d: 3 } }
obj2.c.d = 4;
console.log(obj2); // { a: 1, b:2, c: { d: 4 } }
conosle.log(obj1); // { a: 1, b:2, c: { d: 4 } }

从上述方法可以看出,在合并对象时,如果源对象的子属性是引用类型,则只是浅拷贝引用地址,修改目标对象后源对象也会发生变化。所以 Object.assign() 只能用作合并对象和浅拷贝。

深拷贝(深复制)

有时候我们考虑在复杂类型上需要独立引用地址,相互修改不会受到影响,这时候就可以用深拷贝的方法去完成。

JSON.parse() 和 JSON.stringify()

let obj1 = { a:1, b: '张三', c: true };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 2;
obj1.b = '李四';
obj1.c = false;
console.log(obj1); // { a: 2, b: '李四', c: false }
console.log(obj2); // { a: 1, b: '张三', c: true } 

上述情况使用 JSON 方法也可以完成基本的深拷贝,但是受制于 API 的限制,子属性的数据格式必须要满足 JSON 的解析规则,否则会被省略。

递归循环

function deepClone(target, map = new Map()) {
  if( Object.prototype.toString.call(target) === '[object Object]' ) {
    let cloneTarget = Array.isArray(target) ? [] : {};
    
    if( map.get(target) ) {
      return map.get(target);
    }
    
    map.set(target, cloneTarget);
    
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key], map);
    }
    
    return cloneTarget;
  } else {
    return target;
  }
}

递归循环可以基本完成一个深拷贝对象,但是也只是考虑了对象和数组的情况,实际的引用类型还有很多。

另外

let obj1 = { a: 1, b: '张三', c: true };
let obj2 = Object.assign({}, obj1);
obj1.a = 2;
obj1.b = '李四';
obj1.c = false;
console.lof(obj1); // { a: 2, b: '李四',c: false }
console.log(obj2); // { a: 1, b: '张三', c: true }

如果需要复制的对象子属性都是基本类型,也可以使用 Object.assign() 方法。

最后

上面介绍的深拷贝方法都不算很完美,在实际的生活中也很难遇到所有情况都考虑的需求,我们可以借助一下第三方库来解决,比如:Lodash-cloneDeep


Comments

Leave a Comment