2022-07-31 17:37:14 +08:00
|
|
|
|
## 前言
|
|
|
|
|
|
|
|
|
|
在 JavaScript 的编程中经常需要对数据进行复制,这就涉及到浅拷贝和深拷贝,是非常重要的概念。
|
|
|
|
|
|
|
|
|
|
## 浅拷贝
|
|
|
|
|
|
|
|
|
|
### 概念
|
|
|
|
|
|
2022-08-05 22:47:51 +08:00
|
|
|
|
创建一个新的对象B,来接收你要重新复制的对象A的值:
|
|
|
|
|
|
|
|
|
|
- 如果对象A里面的属性是基本类型,拷贝的是基本类型的值;
|
|
|
|
|
- 但如果对象A里面的属性是引用类型,拷贝的是内存中的**地址**(不是拷贝**值**)。也就是说,拷贝后的内容和原始内容,指向的是同一个地址。如果一个对象的属性值发生了变化,另一个对象的属性值也会发生变化。
|
|
|
|
|
|
|
|
|
|
浅拷贝在拷贝引用类型的数据时,只拷贝**第一层**的属性,再深层的属性无法进行拷贝。用一个成语形容叫“藕断丝连”。
|
2022-07-31 17:37:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 深拷贝
|
|
|
|
|
|
|
|
|
|
### 概念
|
|
|
|
|
|
|
|
|
|
创建一个新的对象B,来接收你要重新复制的对象A的值:
|
|
|
|
|
|
|
|
|
|
- 在堆内存中开辟了一块全新的内存地址,将对象A的属性完全复制过来。
|
2022-08-05 22:47:51 +08:00
|
|
|
|
- 这两个对象相互独立、互不影响,彻底实现了内存上的分离。
|
2022-07-31 17:37:14 +08:00
|
|
|
|
|
|
|
|
|
下面讲一下实现深拷贝的几种方式。
|
|
|
|
|
|
|
|
|
|
### 方式1:JSON.stringify() 和 JSON.parse()
|
|
|
|
|
|
|
|
|
|
这是最简单的深拷贝方法,先把对象序列化成 json 字符串,然后将JSON 字符串生成一个新的对象。
|
|
|
|
|
|
|
|
|
|
代码实现:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
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]}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
方式1属于乞丐版。缺点是:
|
|
|
|
|
|
|
|
|
|
(1)主要缺点:
|
|
|
|
|
|
|
|
|
|
- 无法拷贝函数、undefined、symbol。经过 JSON.stringify 序列化之后的字符串中这个键值对会消失。
|
|
|
|
|
- 无法拷贝 Map、Set;
|
|
|
|
|
- 无法拷贝对象的循环引用,即 obj[key] = obj。
|
|
|
|
|
|
|
|
|
|
(2)其他缺点:
|
|
|
|
|
|
|
|
|
|
- 拷贝 Date 引用类型会变成字符串;
|
|
|
|
|
- 拷贝 RegExp 引用类型会变成空对象;
|
|
|
|
|
- 无法拷贝不可枚举的属性;
|
|
|
|
|
- 无法拷贝对象的原型链;
|
|
|
|
|
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
|
|
|
|
|
|
|
|
|
|
无法拷贝函数的代码举例:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
const obj = { fn: () => {}, name: 'qianguyihao' };
|
|
|
|
|
console.log(JSON.stringify(obj)); // {"name":"qianguyihao"}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
无法拷贝循环引用的代码举例:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
const obj = { fn: () => {}, name: 'qianguyihao' };
|
|
|
|
|
obj.self = obj;
|
|
|
|
|
/*
|
|
|
|
|
控制台报错:
|
|
|
|
|
Uncaught TypeError: Converting circular structure to JSON
|
|
|
|
|
--> starting at object with constructor 'Object'
|
|
|
|
|
--- property 'self' closes the circle
|
|
|
|
|
at JSON.stringify (<anonymous>)
|
|
|
|
|
*/
|
|
|
|
|
console.log(JSON.stringify(obj));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
小结:如果你的数据结构是简单的数据类型,使用方式1是最简单和快捷的选择;但如果数据类型稍微复杂一点,方式1 就不行了。
|
|
|
|
|
|
|
|
|
|
### 方式2:手写递归
|
|
|
|
|
|
|
|
|
|
如果只考虑简单的数组、对象,方式2是满足要求的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
const obj1 = {
|
|
|
|
|
name: 'qianguyihao',
|
|
|
|
|
age: 30,
|
|
|
|
|
address: {
|
|
|
|
|
city: 'shenzhen'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const obj2 = deepClone(obj1)
|
|
|
|
|
obj2.address.city = 'beijing'
|
|
|
|
|
console.log(obj1.address.city)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 深拷贝
|
|
|
|
|
* @param {Object} obj 要拷贝的对象
|
|
|
|
|
*/
|
|
|
|
|
function deepClone(obj = {}) {
|
|
|
|
|
// 1、判断是值类型还是引用类型
|
|
|
|
|
if (typeof obj !== 'object' || obj == null) {
|
|
|
|
|
// obj 如果不是对象和数组,或者是 null,就直接return
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2、判断是数组还是对象
|
|
|
|
|
// 初始化返回结果:数组或者对象
|
|
|
|
|
let result
|
|
|
|
|
if (obj instanceof Array) {
|
|
|
|
|
result = []
|
|
|
|
|
} else {
|
|
|
|
|
result = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let key in obj) {
|
|
|
|
|
// 保证 key 不是原型的属性
|
|
|
|
|
if (obj.hasOwnProperty(key)) {
|
|
|
|
|
// 3、递归【关键代码】
|
|
|
|
|
result[key] = deepClone(obj[key])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let obj1 = {
|
|
|
|
|
a:{
|
|
|
|
|
b:1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let obj2 = deepClone(obj1);
|
|
|
|
|
obj1.a.b = 2;
|
|
|
|
|
console.log(obj2); // {a:{b:1}}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
上面的代码,还有一种写法,更容易理解:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
|
|
|
|
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 obj1 = {
|
|
|
|
|
a:{
|
|
|
|
|
b:1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let obj2 = deepClone(obj1);
|
|
|
|
|
obj1.a.b = 2;
|
|
|
|
|
console.log(obj2); // {a:{b:1}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
方式2只考虑了 object 和 Array这种 对普通的引用类型的值,是属于比较基础的深拷贝。缺点是:
|
|
|
|
|
|
|
|
|
|
(1)主要缺点:
|
|
|
|
|
|
|
|
|
|
- 无法拷贝函数 Function。
|
|
|
|
|
- 无法拷贝 Map、Set。
|
|
|
|
|
- 无法拷贝对象的循环引用,即 obj[key] = obj。
|
|
|
|
|
|
|
|
|
|
(2)其他缺点:
|
|
|
|
|
|
|
|
|
|
- 无法拷贝不可枚举的属性以及 Symbol 类型。
|
|
|
|
|
- 无法拷贝 Date、RegExp、Error 这样的引用类型。
|
|
|
|
|
|
|
|
|
|
### 方式3:改进版
|
|
|
|
|
|
|
|
|
|
针对上面几个问题,可以用如下几点改进:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(1)针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
|
|
|
|
|
|
|
|
|
|
(2)当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
|
|
|
|
|
|
|
|
|
|
(3)利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
|
|
|
|
|
|
|
|
|
|
(4)利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
/**
|
|
|
|
|
* 深拷贝
|
|
|
|
|
* @param obj obj
|
|
|
|
|
* @param map weakmap 为了避免循环引用
|
|
|
|
|
*/
|
2022-08-05 22:47:51 +08:00
|
|
|
|
function cloneDeep(obj, map = new WeakMap()) {
|
2022-07-31 17:37:14 +08:00
|
|
|
|
if (typeof obj !== 'object' || obj == null ) return obj
|
|
|
|
|
|
|
|
|
|
// 避免循环引用
|
|
|
|
|
const objFromMap = map.get(obj)
|
|
|
|
|
if (objFromMap) return objFromMap
|
|
|
|
|
|
2022-08-05 22:47:51 +08:00
|
|
|
|
let target = {}
|
2022-07-31 17:37:14 +08:00
|
|
|
|
map.set(obj, target)
|
|
|
|
|
|
|
|
|
|
// Map
|
|
|
|
|
if (obj instanceof Map) {
|
|
|
|
|
target = new Map()
|
|
|
|
|
obj.forEach((v, k) => {
|
|
|
|
|
const v1 = cloneDeep(v, map)
|
|
|
|
|
const k1 = cloneDeep(k, map)
|
|
|
|
|
target.set(k1, v1)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set
|
|
|
|
|
if (obj instanceof Set) {
|
|
|
|
|
target = new Set()
|
|
|
|
|
obj.forEach(v => {
|
|
|
|
|
const v1 = cloneDeep(v, map)
|
|
|
|
|
target.add(v1)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Array
|
|
|
|
|
if (obj instanceof Array) {
|
|
|
|
|
target = obj.map(item => cloneDeep(item, map))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Object
|
|
|
|
|
for (const key in obj) {
|
|
|
|
|
const val = obj[key]
|
|
|
|
|
const val1 = cloneDeep(val, map)
|
|
|
|
|
target[key] = val1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return target
|
|
|
|
|
}
|
|
|
|
|
```
|