对象的合并与拷贝(又称复制或克隆)是前端们平时工作中绕不开的基本操作,使用场景非常多。也许你已经有了自己用惯了的工具方法,但是对于这个话题,你确定自己已经完全了解了吗?
合并与克隆的关系
拷贝可以认为是一种特殊情况下的合并:将一个空对象 {}
作为目标,与一个非空对象合并。
constructor
相同,一个
Person
类的实例被拷贝后应该还是一个
Person
,不能变成
Dog
,更不能变成一个不知道是什么东西的
Object
。
此外,合并和拷贝在方法调用上也有差别。合并一般要求支持多个源对象向目标对象合并,而拷贝的源对象只有一个。
常见的合并与拷贝的方法
JSON.parse(JSON.stringify() )
来实现对象复制。ES5 中增加了原生的
Object.assign
来实现合并。而利用 ES6 中的扩展运算符,调用形如
{…x, …y}
的声明,也能实现对象的合并。
除了这些原生的合并拷贝方法,我还找了jQuery@3.2.1,underscore@1.8.3,lodash@4.16.1
三个大名鼎鼎库中的相关方法。我们接下来将以他们作为例子,详细从合并与拷贝方法的各个维度上来分析。
说明:jQuery.extend
方法有很多调用方式,既可以拷贝又可以合并,所以在两个列表中都出现。另外,lodash
和 underscore
都使用下划线_,这里根据“先来后到”的顺序,用 _ 指代underscore
,用 l 指代 lodash
。
JSON.parse(JSON.stringify())
$.extend
_.clone
l.clone
l.cloneDeep
l.cloneWith
l.cloneDeepWith
{…x, …y}
Object.assign
$.extend
_.extend
_.extendOwn
l.assign
l.assignIn
l.assignWith
l.assignInWith
l.merge
l.mergeWith
拷贝方法分析
对于依赖 JSON 来拷贝对象的 JSON.parse(JSON.stringify())
方法来说,undefined
和 function
类型的属性会被忽略,而 Date
类型的属性则会被转换为字符串,这可能不是我们想要的。
2.是否能够正确处理 constructor?
Person
不能变为
Dog
,也不能变为
Object
。目标对象应该保留源对象的
constructor
。
3.是否是深拷贝?
4.是否支持 customizer?
customizer
是指一个处理方法,允许用户定制拷贝中的处理过程,其作用类似 Array
系列方法中的遍历处理函数。一开始我也没想到这个维度,还是在研究 lodash
相关方法的时候才看到的。不得不说,这是一个很有用的特性。
function customizer(value) {
if (_.isElement(value)) {
return value.cloneNode(false);
}
}
var el = _.cloneWith(document.body, customizer);
方法 | 是否支持处理特殊类型 | 是否能够正确处理constructor | 是否是深拷贝 | 是否支持customizer |
JSON.parse(JSON.stringify()) | 否 | 否 | 是 | 否 |
$.extend | 是 | 否 | 支持(第一个参数为true) | 否 |
_.clone | 是 | 否 | 否 | 否 |
l.clone | 是 | 是 | 否 | 否 |
l.cloneDeep | 是 | 是 | 是 | 否 |
l.cloneWith | 是 | 是 | 否 | 是 |
l.cloneDeepWith | 是 | 是 | 是 | 是 |
underscore
的clone
方法不支持深拷贝,比较弱。
jquery
的extend
方法默认不使用深拷贝,但当第一个参数传入 true 时则使用深拷贝来处理。
- lodash 提供了4个
clone
相关方法。只有 lodash 的 clone 方法正确处理了constructor
,而customizer
也只有lodash
一家独有(两个with 方法)。
合并方法分析:
1.原型属性是否参与合并?
原型属性参与合并时,源对象原型上的属性会被作为目标对象上的普通属性。如:
function Foo() {
this.a = 1;
}
Foo.prototype.b = 2;
let x = new Foo();
assign({}, x);
// {a: 1, b: 2}
字面意思。值 为undefined 的属性是否参与合并。
3.是否递归合并?
首先确认一点,所有的合并操作都不会是“浅”的,都不会直接把引用地址赋给目标对象。但在此基础上,又有不同的合并策略。比如:
let x = {a: {m: 1, n: 2}};
let y = {a: {m: 2, o: 3}};
assign(x ,y);
// 非递归合并
// {a: {m: 2, o: 3}}
// 递归合并
// {a: {m: 2 , n: 2, o: 3}}
4.是否支持 customizer?
方法 | 原型属性是否参与合并 | undefined是否参与合并 | 是否递归合并 | 是否支持customizer |
{…x, …y} | 否 | 是 | 否 | 否 |
Object.assign | 否 | 是 | 否 | 否 |
jQuery.extend | 否 | 否 | 支持(第一个参数为true) | 否 |
_.extend | 是 | 是 | 否 | 否 |
_.extendOwn | 否 | 是 | 否 | 否 |
l.assign | 否 | 是 | 否 | 否 |
l.assignIn | 是 | 是 | 否 | 否 |
l.assignWith | 否 | 是 | 否 | 是 |
l.assignInWith | 是 | 是 | 否 | 是 |
l.merge | 否 | 否 | 是 | 否 |
l.mergeWith | 否 | 否 | 是 | 是 |
- 原生方法中使用
Object.assign
方法和使用扩展操作符完全一样。
- 除了lodash 的
merge
,其余方法都不支持递归合并。
- 除了lodash 的
merge
,其余方法undefined
都参与合并。
- 除了lodash 的三个
with
方法,其余方法都不支持customizer
。
根据上面的两张表,读者可以自行选择合适的合并与拷贝方法了。如果有其他方法,也可以用这些维度来进行分析。