Web/07-JavaScript进阶/02-浅拷贝和深拷贝.md
2022-08-05 22:47:51 +08:00

253 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 前言
在 JavaScript 的编程中经常需要对数据进行复制,这就涉及到浅拷贝和深拷贝,是非常重要的概念。
## 浅拷贝
### 概念
创建一个新的对象B来接收你要重新复制的对象A的值
- 如果对象A里面的属性是基本类型拷贝的是基本类型的值
- 但如果对象A里面的属性是引用类型拷贝的是内存中的**地址**(不是拷贝**值**)。也就是说,拷贝后的内容和原始内容,指向的是同一个地址。如果一个对象的属性值发生了变化,另一个对象的属性值也会发生变化。
浅拷贝在拷贝引用类型的数据时,只拷贝**第一层**的属性,再深层的属性无法进行拷贝。用一个成语形容叫“藕断丝连”。
## 深拷贝
### 概念
创建一个新的对象B来接收你要重新复制的对象A的值
- 在堆内存中开辟了一块全新的内存地址将对象A的属性完全复制过来。
- 这两个对象相互独立、互不影响,彻底实现了内存上的分离。
下面讲一下实现深拷贝的几种方式。
### 方式1JSON.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 以及 -InfinityJSON 序列化的结果会变成 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 为了避免循环引用
*/
function cloneDeep(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj == null ) return obj
// 避免循环引用
const objFromMap = map.get(obj)
if (objFromMap) return objFromMap
let target = {}
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
}
```