JavaScript 手撕深拷贝
# 深拷贝
JS 虽然没有指针,但是存在引用类型,除了基本数据类型 number、string、boolean、null、undefined、symbol、bigint,其他都是引用类型。常见的引用类型包括: object、array、function、map、set、date、regexp 等。
深拷贝主要考校的是对于引用类型的拷贝,对于基本数据类型,直接赋值即可。
因此深拷贝的实现一般需要考虑这几个方面:
基本数据类型。
引用类型的。
2.1 object、array、function
2.2 map、set
2.3 date、regexp
循环引用。
除此之外还有一些进阶选项:
- symbol 作为 key 时的支持。
- 原型拷贝。
- 不可枚举属性属性拷贝。
- 函数的拷贝。
- 二进制数组,如 Int8Array、Uint8Array、Uint8ClampedArray 等
# 实现代码
本代码中未对『进阶选项』进行支持
function deepCopy(data) {
// 定义 Map 防止循环引用
const weakMap = new WeakMap();
// 定义 func 判断值的类型
function type(value) {
return value === null ? 'Null' : value === undefined ? 'Undefined' : Object.prototype.toString.call(value).slice(8, -1);
}
function clone(value) {
// 实际 copy 方法
const copy = function copy(copiedValue) {
if (weakMap.has(value)) {
return weakMap.get(value);
}
weakMap.set(value, copiedValue);
if (value instanceof Map) {
value.forEach((el, key) => {
copiedValue.set(key, clone(el));
});
} else if (value instanceof Set) {
value.forEach(el => {
copiedValue.add(clone(el));
});
} else {
for (const key in value) {
if(bject.prototype.hasOwnProperty.call(value,key)) {
copiedValue[key] = clone(value[key]);
}
}
}
return copiedValue;
};
// 判断值的类型
switch (type(value)) {
case 'Object': return copy(Object.create(Object.getPrototypeOf(value)));
case 'Array': return copy([]);
case 'RegExp':
case 'Date': return new value.constructor(value);
case 'Map': return copy(new Map());
case 'Set': return new Set(value);
default: return value;
}
}
return clone(data);
}
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
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
# 实现思路
# 懒人版思路
JSON.parse(JSON.stringify(obj))
通过 JSON 转换一次即可实现深拷贝,但是实际使用中,有很多情况无法处理,酌情处理。
# 手撕思路
基本类型,基本类型不涉及引用,直接赋值即可。
function,没想到 function 如何产生变更(不认为其有需要深拷贝的场景),其实也可以直接返回。不放心可以采用下面的方式生成新函数。
new Function('return' + func.toString())()
array 数组如果仅涉及基本数据类型,进行解构赋值即可。
[ ...array ]
map、set 遍历 new 一个新的进行赋值。
object 对象,也可以创建一个新的对象,遍历属性进行重新赋值,遍历会遇到以下问题。
创建对象的需要原型保持一致
Object.create(Object.getPrototypeOf(obj))
遍历对象时,会遍历到原型链上的属性,需要过滤掉。
Object.prototype.hasOwnProperty()
如何遍历不可枚举的对象。
Reflect.ownKeys(obj)
遍历以后遇到引用类型,需要递归处理。可能会引出第6点的循环引用问题。
循环引用,用 map、array 等数据结构存储已经处理过的对象,如果碰撞直接返回。这里推荐 weakMap。