0%

JS 深拷贝补充

前言

背面试题,又遇到了这个 JS 深拷贝问题了,但是这次碰到了个特别变态的。

问题

需要能正常的拷贝下面的对象

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
// 测试的obj对象
const obj = {
// =========== 1.基础数据类型 ===========
num: 0, // number
str: "", // string
bool: true, // boolean
unf: undefined, // undefined
nul: null, // null
sym: Symbol("sym"), // symbol
bign: BigInt(1n), // bigint

// =========== 2.Object类型 ===========
// 普通对象
obj: {
name: "我是一个对象",
id: 1,
},
// 数组
arr: [0, 1, 2],
// 函数
func: function () {
console.log("我是一个函数");
},
// 日期
date: new Date(0),
// 正则
reg: new RegExp("/我是一个正则/ig"),
// Map
map: new Map().set("mapKey", 1),
// Set
set: new Set().add("set"),
// =========== 3.其他 ===========
[Symbol("1")]: 1, // Symbol作为key
};

// 4.添加不可枚举属性
Object.defineProperty(obj, "innumerable", {
enumerable: false,
value: "不可枚举属性",
});

// 5.设置原型对象
Object.setPrototypeOf(obj, {
proto: "proto",
});

// 6.设置loop成循环引用的属性
obj.loop = obj;

评价一个深拷贝是否完善,请检查以下问题是否都实现了:

  1. 基本类型数据是否能拷贝?
  2. 键和值都是基本类型的普通对象是否能拷贝?
  3. Symbol 作为对象的 key 是否能拷贝?
  4. DateRegExp 对象类型是否能拷贝?
  5. MapSet 对象类型是否能拷贝?
  6. Function 对象类型是否能拷贝?(函数我们一般不用深拷贝)
  7. 对象的原型是否能拷贝?
  8. 不可枚举属性是否能拷贝?
  9. 循环引用是否能拷贝?

我看了之后惊了 Lodash 都不敢这么卷啊

解决

不多说,上代码

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
function deepClone(target) {
// WeakMap作为记录对象Hash表(用于防止循环引用)
const map = new WeakMap();

// 判断是否为object类型的辅助函数,减少重复代码
function isObject(target) {
return (
(typeof target === "object" && target) || typeof target === "function"
);
}

function clone(data) {
// 基础类型直接返回值
if (!isObject(data)) {
return data;
}

// 日期或者正则对象则直接构造一个新的对象返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data);
}

// 处理函数对象
if (typeof data === "function") {
return new Function("return " + data.toString())();
}

// 如果该对象已存在,则直接返回该对象
const exist = map.get(data);
if (exist) {
return exist;
}

// 处理Map对象
if (data instanceof Map) {
const result = new Map();
map.set(data, result);
data.forEach((val, key) => {
// 注意:map中的值为object的话也得深拷贝
if (isObject(val)) {
result.set(key, clone(val));
} else {
result.set(key, val);
}
});
return result;
}

// 处理Set对象
if (data instanceof Set) {
const result = new Set();
map.set(data, result);
data.forEach((val) => {
// 注意:set中的值为object的话也得深拷贝
if (isObject(val)) {
result.add(clone(val));
} else {
result.add(val);
}
});
return result;
}

// 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
const keys = Reflect.ownKeys(data);
// 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
const allDesc = Object.getOwnPropertyDescriptors(data);
// 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
const result = Object.create(Object.getPrototypeOf(data), allDesc);

// 新对象加入到map中,进行记录
map.set(data, result);

// Object.create()是浅拷贝,所以要判断并递归执行深拷贝
keys.forEach((key) => {
const val = data[key];
if (isObject(val)) {
// 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
result[key] = clone(val);
} else {
result[key] = val;
}
});
return result;
}

return clone(target);
}

// 测试
const clonedObj = deepClone(obj);
clonedObj === obj; // false,返回的是一个新对象
clonedObj.arr === obj.arr; // false,说明拷贝的不是引用
clonedObj.func === obj.func; // false,说明function也复制了一份
clonedObj.proto; // proto,可以取到原型的属性

后记

转载来源

这个写的是真的好。太感谢这位大佬了。