Fork me on GitHub

由浅入深详解深拷贝实现

一、简单实现

简单深拷贝可以分成2步,浅拷贝+递归
浅拷贝时增加判断属性值是否是对象,如果是对象就进行递归操作
这样就实现了深拷贝

首先,写出简单浅拷贝的代码:

1
2
3
4
5
6
7
8
9
function shadowClone (source) {
let target = {};
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
return target;
}

稍微改动,加上是否是对象的判断并在相应的位置使用递归就可以实现简单深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
function deepClone1 (source) {
let target = {};
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object') {
target[key] = deepClone(source[key]); // 递归
} else {
target[key] = source[key];
}
}
}
return target;
}

但是这个简单版存在几个漏洞:

1.没有对传入参数进行校验,传入null时应该返回null而不是{}
2.对于对象的判断逻辑不严谨,typeof null === ‘object’
3.没有考虑到数组的拷贝问题

二、解决三个漏洞

对于是否是对象的判断,常用方法:

1
2
3
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}

但是数组会被排除,因此要用typeof来处理,把数组保留下来

1
2
3
function isObject(obj) {
return typeof obj === 'object' && obj !== null;
}

然后区分数组和对象

1
let target = Array.isArray(source) ? [] : {};

深拷贝代码改造后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone2 (source) {
// 非对象返回自身
if(!isObject(source)) return source;
// 区分数组和对象
let target = Array.isArray(source) ? [] : {};
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}

三、解决循环引用

JSON.parse(JSON.stringify(obj))这种方式深拷贝遇到循环引用会报错:
// TypeError: Converting circular structure to JSON

es6 -> 哈希表

方法:循环检测,设置一个数组或者哈希表存储已经拷贝过的对象,当检测到对象已存在于哈希表时,取出该值并返回即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone3 (source, hash = new WeakMap()) {
if (!isObject) return source;
// 查哈希表
if (hash.has(source)) return hash.get(source);
let target = Array.isArray(source) ? [] : {};
// 存储到哈希表
hash.set(source, target);
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = deepClone3(source[key], hash); // 传入hash表
} else {
target[key] = source[key];
}
}
}
return target;
}

es5 -> 数组

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
/*  数组中存储的对象结构:
uniqueList.push({
source: source,
target: target
});
*/
function deepclone3 (source, uniqueList) {
if (!isObject(source)) return source;
// 初始化数组
if (! uniqueList) uniqueList = [];

let target = Array.isArray(source) ? [] : {};
// 在数组中查找数据
const uniqueData = find(uniqueList, source);
// 数据已存在,返回
if (uniqueData) {
return uniqueData.target;
};
// 数据不存在,保存源数据以及引用
uniqueList.push({
source: source,
target: target
});

for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = deepClone3(source[key], uniqueList); // 传入数组
} else {
target[key] = source[key];
}
}
}

return target;
}

// 辅助方法:用于在数组中查找对象是否存在
function find (arr, item) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}

⚠️惊喜:存储已拷贝过的对象还解决了引用丢失的问题

1
2
3
4
5
6
7
8
9
10
11
12
var obj1 = {};
var obj2 = {a: obj1, b: obj1};

obj2.a === obj2.b;
// true

var obj3 = deepClone2(obj2);
obj3.a === obj3.b;
// false

var obj4 = deepClone3(obj2);
obj4.a === obj4.b;

四、拷贝Symbol

⚠️补课:如何检测出Symbol类型?

Symbol作为属性名。该属性不会出现在for…in ,for…of 循环中,也不会被Object.keys() 和
Object.getOwnPropertyNames() 返回。

可以通过以下两种方式检测:

  • Object.getOwnPropertySymbols(…)
    可以查找一个给定对象的符号属性时返回一个 symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
1
2
3
4
5
6
7
8
9
10
11
12
let obj = {};
let a = Symbol("a"); // 创建新的symbol类型
let b = Symbol.for("b"); // 从全局的symbol注册表设置和取得symbol

obj[a] = "123";
obj[b] = "456";

let objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols) // [Symbol(a), Symbol(b)]
console.log(objectSymbols.length) // 2
console.log(objectSymbols[0]) // Symbol(a)
  • Reflect.ownKeys(…)
    返回一个由目标对象自身的属性键组成的数组,返回值等于
1
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

⚠️补充:Reflect其实就是换一种更加优雅的方式来操作对象
比如判断对象中是否有某个属性

1
2
3
4
5
attr in obj // 传统做法
Reflect.has(obj, attr) // Reflect做法

bar.apply(obj, 'iwen') // 传统做法
Reflect.apply(bar, obj, [1, 2, 3]) // Reflect做法

例如:获取window上绑定的所有事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let getAllWindowEvents = function (w) {
w.listenList = new Set()
let _cache = w. addEventListener
let handler = {
apply(target, thisBinding, args) {
listenList.add(args[0]);
Reflect.apply(_cache, w, args);
}
}
w. addEventListener = new Proxy((event, fn) => {}, handler);
}
getAllWindowEvents(window);

window.addEventListener('click', function() {
console.log('click')
})
window.addEventListener('resize', function() {
console.log('resize')
})

console.log(...listenList) // click resize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {
[Symbol('a')]: 1,
b: 2,
[Symbol()]: 3,
d: 'abc'
};

let sym = Symbol.for('c');
obj[sym] = 4;

Reflect.ownKeys(obj);
// ["b", "d", Symbol(a), Symbol(), Symbol(c)]
/*
注意顺序:
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
*/

拷贝Symbol思路:
检查有没有Symbol属性,如果找到则先遍历处理Symbol情况,然后再处理正常情况。

1.Object.getOwnPropertySymbols(…)方法:

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
function deepClone4 (source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);

let target = Array.isArray(source) ? [] : {};
hash.set(source, target);

// 新增对于Symbol的处理
let symKeys = Object.getOwnPropertySymbols(source);
if (symKeys.length) {
symKeys.forEach(symKey => {
if (isObject(source[symkey])) {
target[symkey] = deepClone4(source[symkey], hash);
} else {
target[symkey] = source[symkey];
}
})
}

for (let key in source) {
if (Object.prototype.getOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = deepClone4(source[key], hash);
} else {
target[key] = source[key];
}
}
}

return target;
}

2.Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepClone4 (source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);

let target = Array.isArray(source) ? [] : {};
hash.set(source, target);

// 使用了 Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可。
Reflect.ownKeys(source).forEach(key => {
if (isObject(source[key])) {
target[key] = deepClone4(source[key], hash);
} else {
target[key] = source[key];
}
});
return target;
}

Reflect.ownKeys() 这种方式的问题在于不能深拷贝原型链上的数据,因为返回的是目标对象自身的属性键组成的数组。如果想深拷贝原型链上的数据用 for..in 就可以了。

⚠️扩展:es6使用展开语法构造字面量数组和对象
1.展开语法-字面量数组

  • 不再需要组合使用 push, splice, concat 等方法
  • 返回的是新数组,对新数组修改之后不会影响到旧数组,类似于 arr.slice()。
  • 展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝。
1
2
3
4
5
var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
a // [[], [2], [3]]
b // [[2], [3]]

这里 a 是多层数组,b 只拷贝了第一层,对于第二层依旧和 a 持有同一个地址,所以对 b 的修改会影响到 a。

2.展开语法-字面量对象
将已有对象的所有可枚举属性拷贝到新构造的对象中,类似于 Object.assign() 方法。

1
2
3
4
5
6
7
8
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// { foo: "baz", x: 42, y: 13 }

Object.assign() 函数会触发 setters,而展开语法不会。有时候不能替换或者模拟 Object.assign() 函数,因为会得到意想不到的结果,如下:

1
2
3
4
5
6
7
8
9
var obj1 = { foo: 'bar', x: 1 };
var obj2 = { foo: 'baz', y: 2 };
const merge = ( ...objects ) => ( { ...objects } );

var mergedObj = merge ( obj1, obj2);
// { 0: { foo: 'bar', x: 1 }, 1: { foo: 'baz', y: 2 } }

var mergedObj = merge ( {}, obj1, obj2);
// { 0: {}, 1: { foo: 'bar', x: 1 }, 2: { foo: 'baz', y: 2 } }

这里实际上是将多个解构变为剩余参数( rest ),然后再将剩余参数展开为字面量对象

五、破解递归爆栈

上面深拷贝的方法都用到了递归,可能会出现一个爆栈问题,错误提示如下:

1
// RangeError: Maximum call stack size exceeded

解决办法:循环+栈代替递归

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
function deepClone5 (x) {
const root = {};
// 栈,存入种子数据
const looplist = [
{
parent: root,
key: undefined,
data: x
}
];

// 直到栈为空,循环结束
while (looplist.length) {
const node = looplist.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;

let res = parent;
if (key !== 'undefined') {
res = parent[key] = {};
}

for (let k in data) {
if (object.prototype.hasOwnProperty.call(data, k)) {
if (isobject(data[k])) {
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
37
38
39
40
41
42
43
44
45
46
47
48
function deepClone6 (source) {
const hash = new WeakMap();
let root = {};

const loopList = [
{
parent: root,
key: undefined,
data: source
}
];

while(loopList.length) {
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;

let res = parent;
if (key !== undefined) {
res = parent[key] = Array.isArray(data) ? [] : {};
}

// 判断哈希表中是否存在
if (hash.has(data)) {
parent[key] = hash.get(data);
break; // 中断本次循环
}

hash.set(data, res);

for (let k in data) {
if (Object.prototype.hasOwnProperty.call(data, k)) {
if (isObject(data[k])) {
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
var obj1 = {};
var obj2 = {a: obj1, b: obj1};

obj2.a === obj2.b;
// true

var obj3 = deepClone6(obj2);
obj3.a === obj3.b;
// true

完美~

参考文章:
https://juejin.im/post/5c45112e6fb9a04a027aa8fe
https://segmentfault.com/a/1190000016672263

本文标题:由浅入深详解深拷贝实现

文章作者:tongtong

发布时间:2019年04月02日 - 20:04

最后更新:2019年04月15日 - 20:04

原始链接:https://ilove-coding.github.io/2019/04/02/由浅入深详解深拷贝实现/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------