How to Clone

日常开发中会涉及到对象拷贝相关的运用,怎么拷贝是个值得探讨的问题。

浅拷贝

JavaScript一共有七种基础类型,分别为string,number,boolean,null,undefined,symbol,bigint,浅拷贝遇到基础类型的值会直接重新拷贝该值,而对于object类型的值,则会拷贝该值的引用地址

Object.assign()

我们可以使用Object.assign({}, targetObject)来进行浅拷贝,如下所示:

1
2
3
4
5
6
7
8
9
10
11
const bar = {
foo: 1n,
bar: {
a: 1,
},
};

const newBar = Object.assign({}, bar);
console.log(newBar.bar === bar.bar); // output: true
newBar.bar.a = 2;
console.log(bar.bar.a); //output: 2

可以观察到对拷贝之后的newBar.bar进行重新赋值操作,依然可以影响到原来的bar.bar,这也是浅拷贝的主要问题。

Object Spread Operation

我们也可以使用对象扩展运算符来进行浅拷贝,如下所示:

1
2
3
4
5
6
7
8
9
10
11
const bar = {
foo: 1n,
bar: {
a: 1,
},
};

const newBar = { ...bar };
console.log(newBar.bar === bar.bar); // output: true
newBar.bar.a = 2;
console.log(bar.bar.a); //output: 2

总的来说,浅拷贝使用简单明了,但是对于非基础类型值的处理而导致的问题,往往让我们放弃浅拷贝而转用深拷贝。

深拷贝

深拷贝则没有上述浅拷贝带来的问题,对于object等类型的值的处理是重新生成相同的值,这样新值和旧值不会相互影响,解决了浅拷贝的问题。

JSON.parse(JSON.stringify(targetObj))

使用JSON相关的方法来对一个对象深拷贝是常用的深拷贝方法之一,优点是调用简单,且速度较快,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
const bar = {
foo: 1,
bar: {
a: 1,
},
bas: [1, 2, 3],
};

const newBar = JSON.parse(JSON.stringify(bar));
console.log(newBar.bar === bar.bar); // output: false
newBar.bar.a = 2;
console.log(bar.bar.a); //output: 1

缺点也比较多,比如对递归引用的值无法处理,以及对Set,Map,Date等类型的对象无法处理等,但是一般来说后端返回的内容不会涉及到如上数据类型,所以JSON.parse(JSON.stringify(targetObj))适用于大部分的情况。

Recursive

当然我们也可以使用递归的思想来解决深拷贝的问题,如下所示:

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
const bar = {
foo: 1,
bar: {
a: 1,
},
bas: [1, 2, 3],
};

function deepClone(target) {
if (typeof target !== 'object') {
return target;
}
if (Array.isArray(target)) {
return target.map(item => deepClone(item));
}
return Object.keys(target).reduce((ret, key) => {
ret[key] = deepClone(target[key]);
return ret;
}, {});
}

const newBar = deepClone(bar);

console.log(newBar.bar === bar.bar); // output: false
newBar.bar.a = 2;
console.log(bar.bar.a); //output: 1

可以看到deepClone简单实现了一个深拷贝,当然这里的deepClone还有很多情况没有考虑,比如对Set等数据结构的处理,这种情况下我们可以借助第三方库如lodash实现,感兴趣的可以看一下lodash关于这块的处理cloneDeep(obj)

structuredClone()

那么有没有比较简单的原生支持的深拷贝,既能避免JSON.parse(JSON.stringify(targetObj))不能处理Set,Map等数据结构的问题,又不需要引入lodash等三方库呢?答案是有的,那就是structuredClone()

structuredClone()目前只有 Firefox 94,Deno 1.14 以及 Node.js 17.0.0 支持,但是实际上相关算法已经在较早之前运用到了 MessageChannelNotification等地方,以MessageChannel为例:

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
function structuredClone(originObj) {
const { port1, port2 } = new MessageChannel();
return new Promise(resolve => {
port1.onmessage = function (event) {
resolve(event.data);
};
port2.postMessage(originObj);
});
}

const obj = {
foo: new Date(),
bar: {
a: new Set([1, 2, 3]),
b: 2,
},
};

const newObj = await structuredClone(obj);

console.log(obj.foo === newObj.foo); // output: false
newObj.foo = new Map([
[1, 2],
[3, 4],
]);
console.log(Object.prototype.toString.call(obj.foo)); // output: [object Date]

可以看到structuredClone()可以正常的处理Set,Date等数据结构,并且成功进行了深拷贝。

总结

  1. 日常开发中JSON.parse(JSON.stringify(targetObj))基本上可以应对大部分情况,且较为高效
  2. 涉及到Map,Set等数据结构的情况下,考虑使用structuredClone(targetObj)
  3. 最后我们仍然可以借用lodash等三方库帮我们处理深拷贝问题

链接

Comments