为什么要区分深拷贝、浅拷贝?

因为我们得数据分为基本数据类型、引用数据类型,复制基本数据类型都会得到一份新得数据,而引用数据类型则不会,所以我们在有些业务场景下需要实现深拷贝。

数据类型

  • 基本数据类型:String、Number、Boolean、Null、Undefind、Symbol 。 直接存储在栈中的数据
  • 引用数据类型:Object、Array、funciton。 直接存储在堆中的数据

基本数据类型和引用数据类型的区别:

保存位置不同:基本数据类型保存在栈内存中,引用数据类型保存在堆内存中,然后在栈内存中保存了一个对堆内存中实际对象的引用,即数据在堆内存中的地址,JS 对引用数据类型的操作都是操作对象的引用而不是实际的对象,如果 obj1 拷贝了 obj2,那么这两个引用数据类型就指向了同一个堆内存对象,具体操作是 obj1 将栈内存的引用地址复制了一份给 obj2,因而它们共同指向了一个堆内存对象;

为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?

1
2
3
4
5
- 堆比栈大,栈比堆速度快;
- 基本数据类型比较稳定,而且相对来说占用的内存小;
- 引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
- 堆内存是无序存储,可以根据引用直接获取;
- 按引用访问:js不允许直接访问保存在堆内存中的对象,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值;

ECMAScript 中所有函数的参数都是按值来传递的,对于原始值,只是把变量里的值传递给参数,之后参数和这个变量互不影响,对于引用值,对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因,因为它们都指向同一个对象;
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当编译器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体

概念

  • 深拷贝:任何层次都做了副本,是深拷贝。返回一个新数组。
  • 浅拷贝:只是把第一层做了一个副本,其他层是共享的地址。
  • 浅拷贝:只复制指向某个对象的指针,而不是复制对象本身,新对象旧对象还是共同享用同一块内存
  • 深拷贝:会另外创造一个一模一样的对象,新对象跟原来的对象不会共享一个内存,修改新对象也不会改到原对象上

含义:假设 B 复制了 A,当修改 A 时,看 B 是否会发生变化,如果 B 也跟着变了,说明这是浅拷贝,拿人手短,如果 B 没变,那就是深拷贝,自食其力。
阐述到栈堆,基本数据类型与引用数据类型,因为这些概念能更好的让你理解深拷贝与浅拷贝

浅拷贝

基本数据类型不在此话题,浅拷贝简单来说就是复制了一份数据得指针指向,旧数据改变,新数据也会随之改变。
下面分别是对象、数据两种常用数据类型的浅拷贝:

1
2
3
4
5
let foo = [0, 1, 2, 3, 4],
let bar = foo;

let foo = { name:"zhangsanfeng",age:101};
let bar = foo;

深拷贝

深拷贝简单来讲就是复制了一份全新得数据,旧数据得改变不会影响新的数据。
我们通过生活中的例子来看一下:

情景一:

张三丰的小名叫张三,他家住在火星,也就是说张三住火星,张三丰也住火星。张三丰搬家的话,张三家的地址也随之改变这叫浅拷贝,反之则是深拷贝。

情景二:

小明有一台最新的 Iphone12 Pro max,小红也有一 台和小明一摸一样的手机,那么这两台手机是同一台吗?结果很显然不是,虽然外观、价格完全相同但是它们确实是唯一的个体。这其实就是我们想要的深拷贝。

解构赋值

下面这种复制对象得方式相当于把对象展开,放入一个新对象容器当中,缺点也比较明显,只能够实现第一层数据得深拷贝,如果第一层得值是引用数据类型就不行了

1
2
let obj1 = { name: "zhangsanfeng", age: 101, hobby: ["chifanfan", "睡觉觉"] };
let obj2 = { ...obj1 };

Object.assign

Object.assgin()用来合并多个对象,并且会返回一个参数对象,默认是浅拷贝
我们可以通过设置第一个参数是空对象得形式来实现深拷贝
同样缺点比较明显,只能够实现第一层数据得深拷贝,如果第一层得值是引用数据类型就不行了

1
2
let obj1 = { name: "zhangsanfeng", age: 101, hobby: ["chifanfan", "睡觉觉"] };
let obj2 = Object.assign({}, obj1);

JSON.parse&JSON.stringify

1
2
3
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj1));
}

这下 b 是完全不受 a 的影响了。
附带说明,JSON.stringify 与 JSON.parse 除了实现深拷贝,还能结合 localStorage 实现对象数组存储。
有兴趣可以阅读博客这篇文章 localStorage 存储数组,对象,localStorage,sessionStorage 存储数组对象

Object.create

类似 Object.assign 只能实现数据第一层的深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj1 = {
a: 1,
b: 2,
c: {
d: 3,
},
};
var obj2 = Object.create(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 4
alert(obj2.c.d); // 4

手动实现 deepClone

基础理解版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let deepClone = function (obj) {
let temp = obj instanceof Object ? {} : [];
if (obj instanceof Object) {
for (let key in obj) {
var prop = obj[key]; // 避免相互引用造成死循环,如obj1.a=obj
if (prop == obj) {
continue;
}
if (typeof prop === "object") {
if (prop instanceof Array) {
temp[key] = [...prop];
} else {
temp[key] = { ...prop };
}
} else {
temp[key] = prop;
}
}
} else {
obj.forEach((itm) => {
if (typeof itm === "object") {
temp.push(deepClone(itm));
} else {
temp.push(itm);
}
});
}
return temp;
};

高端分享版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepCopy(obj1) {
var obj2 = Array.isArray(obj1) ? [] : {};
if (obj1 && typeof obj1 === "object") {
for (var i in obj1) {
var prop = obj1[i]; // 避免相互引用造成死循环,如obj1.a=obj
if (prop == obj1) {
continue;
}
if (obj1.hasOwnProperty(i)) {
// 如果子属性为引用数据类型,递归复制
if (prop && typeof prop === "object") {
obj2[i] = prop.constructor === Array ? [] : {};
arguments.callee(prop, obj2[i]); // 递归调用
} else {
// 如果是基本数据类型,只是简单的复制
obj2[i] = prop;
}
}
}
}
return obj2;
}

lodash

1
2
3
4
5
6
7
8
9
var _ = require("lodash");
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3],
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

slice 是否为深拷贝

1
2
3
4
5
6
// 对只有一级属性值的数组对象使用slice
var a = [1, 2, 3, 4];
var b = a.slice();
b[0] = 2;
alert(a); // 1,2,3,4
alert(b); // 2,2,3,4
1
2
3
4
5
6
// 对有多层属性的数组对象使用slice
var a = [1, [1, 2], 3, 4];
var b = a.slice();
b[1][0] = 2;
alert(a); // 1,2,2,3,4
alert(b); // 1,2,2,3,4

实际开发中也是非常有用的,后台返回了一堆数据,你需要对这堆数据做操作,但多人开发情况下,你是没办法明确这堆数据是否有其它功能也需要使用,直接修改可能会造成隐性问题,深拷贝能帮你更安全安心的去操作数据,根据实际情况来使用深拷贝,大概就是这个意思。