【前端进阶】深拷贝和浅拷贝的区别是什么?如何实现一个深拷贝?

背景:由高级前端工程师@刘小夕 在github上发起的一个开源项目:每个工作日发布一个前端相关的问题,每周进行一次汇总。我觉得很不错,也参与其中,如果你也感兴趣,可以一起参与,项目地址:https://github.com/YvetteLau/Step-By-Step

首先要明白,深拷贝浅拷贝只针对复杂类型(ObjectArray)来说的。

对于复杂类型而言,一个对象创建的时候会在内存中分配两块空间,一个在栈内存中存储对象的引用指针,一个在堆内存中存储对象真实的数据。这时候会有一个问题,你拷贝的只是这个引用指针,还是连同它的真实数据一起拷贝,所以才会有深浅拷贝一说。

=赋值

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的引用地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

let obj = {
  age: 28
}

let obj2 = obj;
obj2.age = 30;

console.log(obj.age); // 30
console.log(obj2.age); // 30

上面代码说明了objobj2是指向的同一个对象,一个对象作出改变,另一个也会跟着改。

浅拷贝

在了解到浅拷贝深拷贝之前, 我以为=赋值就是拷贝了(捂脸)。。。,其实不然。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(复杂类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

要实现浅拷贝也比较简单:

可以使用Object.assign()、扩展运算符...Array.prototype.slice()Array.prototype.concat()等,这里我们拿其中一个来举例:

let obj = {
  age: 28
}

let obj2 = Object.assign({}, obj);
obj2.age = 30;

console.log(obj.age); // 28
console.log(obj2.age);  // 30

通过这个例子,可以很清楚的知道浅拷贝=赋值的区别了:浅拷贝可以实现基本类型的拷贝且不会影响到原对象。

我们在上面代码的基础上再做一下修改:

let obj = {
  age: 28,
  child: {
    age: 8
  }
}

let obj2 = Object.assign({}, obj);
obj2.child.age = 10;

console.log(obj.child.age); // 10
console.log(obj2.child.age);  // 10

上面的代码说明:浅拷贝只能实现第一层基本类型的拷贝,当对象包含子对象时,实际上拷贝的只是引用地址,无论哪个对象作出改变,另一个都会跟着改变。

这时候就需要使用深拷贝了。

深拷贝

简单理解就是:可以实现多层数据的拷贝,拷贝后的对象是一个全新独立的对象,重新为其分配内存空间,跟原对象不会相互影响。

使用 JSON.parse(JSON.stringify(obj))

实现深拷贝最简单的应该是使用JSON.parse(JSON.stringify(obj))这个方法。

let obj = {
  age: 28,
  child: {
    age: 10
  }
}

let obj2 = JSON.parse(JSON.stringify(obj));
obj2.age = 30;
obj2.child.age = 12;

console.log(obj.age);  // 28
console.log(obj2.age);  // 30

console.log(obj.child.age);  // 10
console.log(obj2.child.age);  // 12

但是该方法也是有局限性的(来自某大佬的小册内容):

  • 会忽略undefined
  • 会忽略symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 原型链上的属性无法获取(来自小姐姐的答案)
let obj = {
  age: undefined,
  sex: Symbol('man'),
  jobs: function () {},
  name: 'dazhi'
}

let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);  // {name: "dazhi"}

可以看到,上述情况中,undefinedsymbol函数都被忽略掉了。

let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3
  }
}

obj.c = obj.b;
obj.b.c = obj.c;

let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);

如果你有这么一个循环引用对象,你会发现并不能通过该方法实现深拷贝,会报错。

function Super() {

}

Super.prototype.location = 'NanJing';

function Child(name, age, hobbies) {
    this.name = name;
    this.age = age;
}

Child.prototype = new Super();

let obj = new Child('Yvette', 18);
console.log(obj.location); //NanJing

let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);//{ name: 'Yvette', age: 18}
console.log(newObj.location);//undefined;原型链上的属性无法获取

上面代码说明:JSON.parse(JSON.stringify(obj))无法拷贝原型链上的属性。

实际开发中,可以直接使用Lodash库的深拷贝方法

Lodash深拷贝方法源码地址:https://github.com/lodash/lodash/blob/master/.internal/baseClone.js

总结

和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含对象
赋值 改变会使原数据一同改变 改变会使原数据一同改
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改
深拷贝 改变不会会使原数据一同改变 改变不会使原数据一同改

参考:

浅拷贝与深拷贝