# js如何实现深拷贝
js中实现深拷贝,主要包括下面几种方法:
- 借助JSON的stringify与parse方法,对一个对象进行序列化与反序列化。
- 递归拷贝一个对象的属性与值,到一个新的对象里。其中在拷贝值的时候,需要考虑这个值是普通类型,还是引用类型。
# JSON.stringify
使用JSON.stringify
,JSON.parse
来拷贝一个对象时,有如下问题:
- 它会丢失值为
function
、undefined
、Symbol
,不可枚举
类型的值; date
、RegExp
等类型的值转换错误:- 其中Date类型的对象,默认转为了日期字符串。正则类型的对象,转成了空对象;
- new Number(1),new Boolean(false)等对象,错误的转成了相应的字符串、布尔值;
- 不能解决循环引用的对象(抛错);
看如下代码:
var source = {
a1: undefined,
a2: null,
fn: function () {}, // 函数
reg: /^path$/, // 正则
date: new Date(), // 日期
name: "jack",
a3: 123,
num2: new Number(2), // 被转成了数字2
book: { title: "You Don't Know JS", price: "45" }
};
// 循环引用
//source.ref = source;
console.log(source, JSON.parse(JSON.stringify(souce)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果对象中存在循环引用,则直接报错:
所以,对于普通对象而言,我们直接使用这种方式是没有问题的。一旦对象中出现了上述的值,则该方法不可行。
# 递归拷贝(循环拷贝)
既然JSON.stringify
无法满足我们的要求,我们可以换一种思路。对一个对象进行递归遍历,分别将值拷贝到新的对象上,就可以达到我们深拷贝的要求,这里要注意以下几点:
- 获取对象的key时,需要使用Reflect.ownKeys(source) (opens new window):
- Object.keys (opens new window):只能获取对象本身可枚举的key;
- Object.getOwnPropertyNames (opens new window):获取对象本身可枚举及不可枚举属性,但是不能获取Symbol类型的key;
- Object.getOwnPropertySymbols (opens new window):只能获取Symbol类型的key;
- 在处理循环引用问题时,需要用到WeakMap (opens new window)(不能使用Map),目的是节约内存开销:
- WeakMap的key,必须是一个对象;
- WeakMap对于key的引用,是一个弱引用。即,一旦这个key对象被垃圾回收了,weakMap中的这个key就不存在了。 - 在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
- 关于WeakMap弱引用的详细介绍,可参考ES6 系列之 WeakMap (opens new window)及理解 WeakMap 的 weak (opens new window)。
- 在循环对象的keys时,推荐使用for循环,相比for of、forEach,效率更高,不推荐使用for in。当对象较大时,
for in
循环的速度很慢。关于执行循环的执行效率,可以这2篇参考:JS数组循环的性能和效率分析 (opens new window),Which is faster: for, for…of, or forEach loops in JavaScrip (opens new window)。
在遍历对象的key时,有2种方式,一种是深度优先(DFS),一种是广度优先(BFS)。关于二者的区别,大家可以自行查阅。
拷贝之前,先定义一些基础工具方法:
const mapType = '[object Map]';
const setType = '[object Set]';
const arrayType = '[object Array]';
const objectType = '[object Object]';
const argsType = '[object Arguments]';
// 可遍历对象
const itTypes = [mapType, setType, arrayType, objectType, argsType];
function isObject(obj) {
let type = typeof obj;
return (type === "object" || type == "function") && obj != null;
}
function getType(obj) {
return Object.prototype.toString.call(obj);
}
// 不可遍历对象
function cloneOtherType(target, type) {
// 特殊对象: Date, RegExp, 以及"new Number(1)"这种通过构造函数创建的对象类型
if (typeof target === "function") {
// 函数, 不做处理直接返回(第三方库都没做处理), 或者返回一个新函数, 内部调用原来的函数
// let fn = function () {
// target.apply(this, arguments);
// }
// return fn;
return target;
}else if (type == '[object RegExp]') { // RegExp
return new RegExp(target);
}else if (type == '[object Date]') { // Date
return new Date(target);
} else { // new Number(1), new String('jack')这种特殊的对象类型
// return new target.constructor(target);
// 等效于上面的写法
return Object(Object.prototype.valueOf.call(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
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
# 深度优先DFS
// DFS
function deepClone_DFS(target, weakMap = new WeakMap()) {
// 原始类型:symbol, string、number、boolean、null、undefined
if (!isObject(target)) return target;
let cloneTarget;
const type = getType(target);
// 可遍历对象:Map、Set、Object、Array、Arguments
if (itTypes.includes(type)) {
cloneTarget = new target.constructor();
} else {
// 非可遍历对象:Date, RegExp, new Number(100), new String('jack'), 以及function等
return cloneOtherType(target, type);
}
if (weakMap.has(target)) return weakMap.get(target); // 处理循环引用值
weakMap.set(target, cloneTarget);
// 可遍历对象:Map、Set、Object、Array、Arguments
// Map, Set: 迭代克隆
if (type == setType) { // Set
target.forEach(value => {
cloneTarget.add(deepClone_DFS(value, weakMap)) });
return cloneTarget;
}
if (type == mapType) { // Map
target.forEach((value, key) => {
cloneTarget.set(key, deepClone_DFS(value,weakMap));
});
return cloneTarget;
}
// Object, Array, Arguments: 遍历克隆
// let keys = Object.keys(target);
let keys = Reflect.ownKeys(target); // 包括可枚举、不可枚举、Symbol属性
// let keys = Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));
for (let key of keys) { // for of
cloneTarget[key] = deepClone_DFS(target[key], weakMap);
}
return cloneTarget;
}
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
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
如果对象的层级够深时,使用深度优先的策略,可能会出现爆栈的情况(执行栈的大小是有限制的,chrome浏览器大概在层级深度为6000左右出现爆栈的情况),此时,我们可以考虑使用广度优先
的策略。
由对象层级深而导致深拷贝爆栈的情况,在真实开发中,基本不会出现。如果真出现了,说明这个对象的设计是不合理的。
# 广度优先BFS
利用一个数组loopList,在循环对象的key时,不停向里面添加新的对象,直到整个loopList循环完成。无论层级多深,都不会有爆栈的情况。
var a = {
a1: 1,
a2: {
b1: 1, b2: { c1: 1 }
}
};
function clone_BFS(target) {
let root = {};
const loopList = [
{ parent: root, key: undefined, data: target }
];
while(loopList.length) {
let node = loopList.pop();
let { parent, key, data } = node;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if(typeof key != 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if(data.hasOwnProperty(k)) {
if(typeof data[k] === 'object') {
loopList.push({
parent: res,
key: k,
data: data[k]
});
}else {
res[k] = data[k];
}
}
}
}
return root;
}
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
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
# 参考
js数据类型转换 →