知识点-深浅拷贝

转载: https://lixuguang.github.io/2020/01/02/FE-guide-new/
作者: 李旭光

你的就是我的鲁迅

拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

浅拷贝

Object.assign

首先可以通过 Object.assign 来解决这个问题。

1
2
3
4
5
6
7
8
// js代码

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // => 1

展开运算

当然我们也可以通过展开运算符(…)来解决

1
2
3
4
5
6
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // => 1

Array

1
2
arr.slice()
arr.concat()

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决,这也是最好用最简单的方法,俗称乞丐版。

乞丐版

1
2
3
4
5
6
7
8
9
10
11
// js代码

let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
    举个栗子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// js代码

let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c

let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj) // => Uncaught TypeError: Converting circular structure to JSON

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

1
2
3
4
5
6
7
8
9
10
// js代码

let a = {
age: undefined,
sex: Symbol('fmale'),
jobs: function() {},
name: 'lixuguang'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // => {name: "lixuang"}

你会发现在上述情况中,该方法会忽略掉函数和 undefined 。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。

那么是否可以解决函数和循环引用的问题呢?答案是肯定可以解决,接下来是基础版本的改造

基础版本

1
2
3
4
5
6
7
8
9
10
11
function myClone(target){
if(typeof target === 'object'){ // 判断传入目标是否是object类型
let cloneTarget = {}; // 创建克隆对象
for(const key in target){ // 遍历目标对象
cloneTarget[key] = myClone(target[key]) // 递归调用 clone 方法
}
return cloneTarget;
} else {
return target // 如果不是 object 返回
}
}

写到这里已经可以帮助你应付一些面试官考察你的递归解决问题的能力。但是显然,这个深拷贝函数还是有一些问题。
这里只考虑了对象,没有考虑数组。
下面我们来做一个强化版的深拷贝,同时考虑对象、数组还有循环引用的问题。

强化版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myClone(target, map = new WeakMap()) { // WeakMap => 键对象弱引用, 可被垃圾回收
if(typeof target === 'object'){ // 判断是否是对象
let cloneTarget = Array.isArray(target) ? [] : {}; // 判断是是数组还是对象
if(map.get(target)) {
return target;
}

map.set(target, cloneTarget);

for(const key in target) {
cloneTarget[key] = myClone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}

当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// js代码

function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}

var obj = {
a: 1,
b: {
c: b
}
}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()

深拷贝实现方式2,可以深拷贝 functionsymbol,等等,堪称终极版

终极版

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// js代码

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}

function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return target;
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

// 调用方法
clone(target);