JavaScript深拷贝,看这一篇就够啦!

大家好我是雪人⛄

最近面试的时候被问到了深拷贝,我自信满满的写出了使用JSON的快捷方法,与递归深拷贝方法(只写了基础版的拷贝对象)。然后…

面试官:如果传入的是Map呢?⛄:那可以判断一下,加一个 clone Map 的。

面试官:如果传入的是Set呢?⛄:那可以判断一下,加一个 clone Set 的。

面试官:如果传入的是Date RegExp Function呢?⛄:嗯?

面试官:如果 Obj 中含有 Symbol 为 key 的呢?⛄:嗯?嗯?嗯?

面试官:如果有循环引用怎么办? ⛄:☞🤡👈

我直接呆住😳,这确实没看过🤡,痛定思痛,火速补习了一波,分享给大家。

数据类型

在实现深拷贝之前,我们需要先了解一下JS的数据类型,一共有两大类。

  • 基本数据类型:number,string,boolean,undefined,null,symbol,bigint
  • 引用数据类型:object,function,array,map,set等..(都是对象)

那么他们有什么区别呢?

基本数据类型在赋值的时候会直接创建一个新的栈地址去存放,改变值的时候也是直接改变这个栈中的值。

1
2
3
4
5
let a = 10
let b = a
b = 11
console.log(a) // 10
console.log(b) // 11

但是引用数据类型是存放在堆中的,我们使用的变量是一个存放在栈中的“指针”,这个指针指向堆中的地址。

1
2
3
4
5
6
7
let a = {
value: 10
}
let b = a
b.value = 11
console.log(a) // {value:11}
console.log(b) // {value:11}

我们发现如果是引用数据类型改变 b 的值 a 的值同样被改变,但是基本数据类型不会,这就是我们刚刚说到的堆和栈的问题。

深拷贝

了解了数据类型的知识后,我们有时候会需要在一个对象上做一些更改,跟原对象做对比展示,这个时候我们就需要用到深拷贝得到一个新的对象改变他才不会改变原对象啦。

基本数据类型直接返回即可,无需特殊处理。

引用数据类型我们也提到,有很多种:普通的 Object,正则对象RegExp,Array,Map,Set,Date,Array,Function。我们需要分类处理,不同的引用数据类型有不同的处理方式,接下来就给大家实现一下。

是否为引用数据类型

  • 我们先封装一个方法用于判断是否为引用数据类型,便于之后使用。
  • 使用 typeof 判断的只有时候 object 或者 function 是引用数据类型,但是我们需要注意 null,typeof null == object,所以我们需要加一个 target !== null
1
2
3
function isObject(target) {
return (typeof target === 'object' && target !== null ) || typeof target === 'function'
}

基本数据类型

  • 基本数据类型直接返回即可
1
2
3
function deepClone(target) {
if(!isObject(target)) return target
}

Date RegExp

  • 日期对象和正则对象可以将其传入对应的构造器重新构造
1
2
3
4
function deepClone(target) {
// ...
if([Date, RegExp].includes(target.constructor)) return new target.constructor(target)
}

Function

  • 函数我们也需要用到函数的构造函数😂

我们可以看一下使用构造函数构造出的函数是什么

1
2
3
4
5
6
7
8
let fun = new Function('return 1')
console.log(fun.toString())
/*
function anonymous(
) {
return 1
}
*/

从上面的代码可以发现构造出的函数就是将我们传入的字符串套进一个匿名函数中,那么我么就可以利用这个构造函数去深拷贝一个含函数

但是被 clone 的函数 toString 后也有一个 function 关键字,嵌套进匿名函数里面怎么能用呢?

我们可以加一个 return 让匿名函数把我们要拷贝的函数返回出去😀,然后调用这个匿名函数就可以得到新的被克隆的函数啦

1
2
3
4
5
6
function deepClone(target) {
// ...
if (target instanceof Function) {
return new Function('return ' + target.toString())()
}
}

Map Set

  • Map Set 我们直接使用迭代器遍历创建即可,遇到 value 为引用数据类型继续递归调用我们的深拷贝函数
  • 这里用到了 clone 函数,后面我们会讲解封装,大家先理解为递归调用深拷贝引用数据类型即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepClone(target) {
// ...

// Map 对象使用迭代器迭代
if (target instanceof Map) {
let newMap = new Map()
for (const [key, val] of target) {
if (isObject(val)) newMap.set(key, clone(val))
else newMap.set(key, val)
}
return newMap
}

// Set 对象使用迭代器迭代
if (target instanceof Set) {
let newSet = new Set()
for (const val of target) {
if (isObject(val)) newSet.add(clone(val))
else newSet.add(val)
}
return newSet
}
}

Array

  • Array也是直接遍历即可,遇到引用数据类型直接递归调用深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepClone(target) {
// ...

// Array 直接遍历即可
if (target instanceof Array) {
const n = target.length
let newArray = new Array(n)
for (let i=0;i<n;i++) {
const item = target[i]
if (isObject(item)) newArray[i] = clone(item)
else newArray[i] = item
}
return newArray
}
}

Object

  • 普通 Object 我们要考虑以 string 和 symbol 为 key 值的两种情况,所以要使用 Reflect.ownKeys 获取 key 值,返回一个数组能得到所有 key 值。
  • 遇到 key 为引用数据类型继续递归调用 遇到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone(target) {
// ...

// Object 对象
// 考虑 symbol 与 string 为 key
const keys = Reflect.ownKeys(target)
let newObj = {}
for (const key of keys) {
const val = target[key]
if (isObject(val)) newObj[key] = clone(val)
else newObj[key] = val
}
return newObj
}

循环引用

在 Object 对象中我们还需要判断一种特殊情况。我们看一看以下代码。

1
2
3
4
5
6
7
let a = {
b: null
}
let b = {
a: a
}
a.b = b

上面这种情况 a 和 b 循环引用,我们深拷贝的时候就会不断递归 a 和 b 最后导致栈溢出然后报错。

我们可以使用 WeakMap 来将 clone 过的对想保存 已经存在在 WeakMap 中的对象就无需再次 clone 直接返回结束递归即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function deepClone(target) {
// 防止循环引用导致栈溢出 weakMap可以防止内存泄漏
// 需要一个全局变量来保存 这里我们再封装一个 clone 函数进行深拷贝 创建闭包
const hashMap = new WeakMap()
function clone(target) {
// ...

// 考虑循环引用问题 如果该对象已存在,则直接返回该对象
if (hashMap.has(target)) return hashMap.get(target)

// ...
// Object 保存在 weakMap 中 防止之后的循环引用
hashMap.set(target, newObj)
return newObj
}

// 最后再返回调用一下
return clone(target)
}

最终代码

最终代码如下

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function deepClone(target) {
// 防止循环引用导致栈溢出 weakMap可以防止内存泄漏
const hashMap = new WeakMap()

// 判断是否为引用数据类型
function isObject(target) {
return (typeof target == 'object' && target!==null || typeof target == 'function')
}

// 主要 clone 函数
function clone(target) {
// 非引用数据类型直接返回即可
if (!isObject(target)) return target

// 考虑循环引用问题 如果该对象已存在,则直接返回该对象
if (hashMap.has(target)) return hashMap.get(target)

// Date 与 RegExp 对象可直接使用构造器构建
if ([Date, RegExp].includes(target.constructor)){
return new target.constructor(target)
}

// Function 可以使用构造器构造
if (target instanceof Function) {
return new Function('return ' + target.toString())()
}

// Map 对象使用迭代器迭代
if (target instanceof Map) {
let newMap = new Map()
for (const [key, val] of target) {
if (isObject(val)) newMap.set(key, clone(val))
else newMap.set(key, val)
}
return newMap
}

// Set 对象使用迭代器迭代
if (target instanceof Set) {
let newSet = new Set()
for (const val of target) {
if (isObject(val)) newSet.add(clone(val))
else newSet.add(val)
}
return newSet
}

// Array 数组使用迭代器迭代
if (target instanceof Array) {
const n = target.length
let newArray = new Array(n)
for (let i=0;i<n;i++) {
const item = target[i]
if (isObject(item)) newArray[i] = clone(item)
else newArray[i] = item
}
return newArray
}

// Object 对象
// 考虑 symbol 与 string 为 key
const keys = Reflect.ownKeys(target)
let newObj = {}
for (const key of keys) {
const val = target[key]
if (isObject(val)) newObj[key] = clone(val)
else newObj[key] = val
}
// Object 保存在 weakMap 中 防止之后的循环引用
hashMap.set(target, newObj)
return newObj
}

return clone(target)
}

如果对你有帮助的话,请帮我点个赞吧😀