1、对象拷贝、浅拷贝与深拷贝问题
JS中对象之间的赋值采用的是引用拷贝的方法。在理解这个之前,需要先理解JS运行时的堆栈空间。对象数据存放在堆内存中,对象变量存放在栈内存中,对象变量通过引用数据的堆地址实现对象访问。
与基本类型不同,对象之间的赋值,是拷贝了堆内存的地址空间,结果是两个变量指向了同一个对象实体,也称共享,任何一个对对象的修改都会影响另一个变量。
let obj1 = {
value: 1
};
let obj2 = obj1;
obj2.value = 2;
console.log(obj1.value); //2
很多时候,这不是我们想要的效果。我们希望克隆出来的值不会影响到原来的对象。
为了实现对象之间互不影响,首先到的方法是对对象的每一个属性都做一次拷贝,类似下面的做法。
function shallowClone(item) {
let clone = {};
for (let prop in item) {
clone[prop] = item[prop];
}
return clone;
}
测试一下:
let obj1 = {
value: 1
};
let obj3 = shallowClone(obj1);
console.log(obj3.value); //1
obj3.value = 2;
console.log(obj1.value); //1
It works!是的,看上去是,让我们看一个稍微复杂的对象。
let compObj1 = {
value: 1,
param: {
id: 1314,
num: 3
}
}
let compObj2 = shallowClone(compObj1);
console.log(compObj2); //{ value: 1, param: { id: 1314, num: 3 } }
compObj2.param.num = 4;
console.log(compObj1.param.num);//4
同样的问题又出现了。原因在于compObj1存在值为对象类型的属性。在shallowClone()中对每一个属性采用赋值拷贝,如果属性是对象类型,也只是得到了引用。
我们也称这种拷贝为浅拷贝。也就是说,浅拷贝事实上只拷贝了对象的最外层属性。与之相对的,如果我们得到的拷贝对象在每一层属性上都不共享,就称为深拷贝,深拷贝是一种彻底的克隆,源对象与克隆对象不会相互影响。
为了实现深拷贝,我们可以采取对对象属性进行递归拷贝的方法。
function deepClone(item) {
let clone = {};
for (let prop in item) {
clone[prop] = (item[prop] instanceof Object ?
deepClone(item[prop]) :
item[prop]);
}
return clone;
}
再使用上面的例子测试,证实确实是对param进行了拷贝。
尽管上面看似实现了对象的拷贝,但那不过是我为了解释简单处理,忽略和很多细节。问题远没有那么简单。
拷贝对象与原对象的一致性。上面的代码代码使用for...in循环遍历属性,但是for...in列出了对象自身及其原型链上的可枚举属性,也就是说不可枚举的属性没有被拷贝,原型也没有得到拷贝。
拷贝方法的通用性。在上面的例子中,只考虑了对象的情况,但实际中,不可避免有人把基本类型作为item传入,这时会得到一个默认对象,显然不合理。另外,我们应该注意到,数组也存在深浅拷贝的问题,当传入数组时,我们希望得到一个拷贝的数组。
拷贝方法的鲁棒性。它是否适用于所有的对象的拷贝?显然,上面的例子并不满足,当对象出现引用循环时,deepClone()将无限递归。
不幸的是,并不像语言一样,JS中对象拷贝的问题并没有一个通用的解决方案。但是,对特定的对象结构,不难找到合适的拷贝方案。下面,对一些常见的拷贝方法做个总结和比较。
2、深浅拷贝的实现
在探讨拷贝之前,我觉得有必要说明几个问题。
你是否真的需要深拷贝? 如果确定对象属性值都是基本类型,或者不会对对象的深层属性进行修改,这些情况下,浅拷贝就足够了。
你需要拷贝哪些内容? 一个对象的属性是复杂的,有自身属性和原型链上的属性,有可枚举属性和不可枚举属性,有字符串属性和Symbol属性,在选择拷贝方案时,请确定你的目标对象具有哪些属性,以及你需要拷贝哪些属性。
可能需要哪些拷贝数据类型?一些特定的类型可能需要特殊的拷贝处理,如Data, RegExp。
2.1 浅拷贝
2.1.1、利用Object.assign()进行浅拷贝
Object.assign(target, ...sources)实现将一组源对象中的自身可枚举属性(包括Symbol属性)复制到目标对象上。我们可以利用这个特点,进行浅拷贝,只需要一行代码。
let copyObj = Object.assign({}, sourceObj);
很多时候,这种拷贝是足够的,它的局限性在于:
只拷贝了自身的可枚举属性,没有拷贝正确的原型和不可枚举属性。
IE不兼容
2.1.2、利用展开语法实现浅拷贝
ES6中的展开语法...能方便地进行对象属性复制,同assign(),它拷贝了对象自身的可枚举属性。
let copyObj = {...target};
相当简洁易懂有没有,这也是我个人比较喜欢的拷贝方式。更强大的在于,你可以决定你要拷贝哪些属性:
let copyObj={};
{copyObj.prop1:prop1, copyObj.prop2:prop2 } = {...target}
另外,展开语法也能进行数组的拷贝:
let newArr = [...arr];
2.1.3、借助Object.create()实现浅拷贝
利用Object.create(), 我们能拷贝出一个高度相似的对象:
let clone = Object.create(
Object.getPrototypeOf(target),
Object.getOwnPropertyDescriptors(target)
);
这种拷贝在对象与对象直接几乎是完美的,正确的原型,正确的属性。但遗憾数组拷贝的结果会变成一个对象,尽管数组的访问方式和方法都可以,但Array.isArray(clone)===false
注意,具有相同属性的对象,同名属性,后边的会覆盖前边的。
Vue中的使用技巧
由于Object.assign()有上述特性,所以我们在Vue中可以这样使用:
Vue组件可能会有这样的需求:在某种情况下,需要重置Vue组件的data数据。
此时,我们可以通过this.$data获取当前状态下的data,通过this.$options.data()获取该组件初始状态下的data。
然后只要使用Object.assign(this.$data, this.$options.data())就可以将当前状态的data重置为初始状态,
非常方便!
2.1.4、手动实现浅拷贝
如果Object.assign()不适用,你可能需要自己定制自己的拷贝函数,类似shallowClone()的例子。不过,现在,让我们来优化一下代码。
一致性:为了更具有通用性,这里尽可能的保证拷贝对象和原对象的一致性。所以,我们会拷贝对象原型以及所有的对象属性。
通用性:只实现数组和对象类型的拷贝,其它类型,我们只返回原来的对象即可。
function shallowClone(target) {
//排除非对象和非数组变量
if (!(target instanceof Object)) return target;
let clone = (Array.isArray(target) ? [] : Object.create(target.__proto__))
let keys = Reflect.ownKeys(target); //保证能获取到所有自身属性
for (let k of keys) {
clone[k] = target[k];
}
return clone;
}
需要注意的是,上面的方式是支持数组的(经过验证)。数组时特殊的对象,arr instanceof Object的结果为true。在for...of循环内进行数组内容或属性的拷贝。在拷贝数组时,不能直接返回数组的副本是因为数组上可能挂有其它的属性。
再次提醒,以上只是提供一种思路,你应该按自己的需要实现拷贝方法。
2.2、深拷贝
2.2.1、借助JSON转化实现深拷贝
使用JSON的解析功能可以快速进行深拷贝,很多时候,这是拷贝对象时的最先想到的方法。
let copyObj = JSON.parse(JSON.stringify(target));
得益于Json格式对数据的处理,这个方法在很多时候都能派上用场。但是,也存在问题:
对某些数据不支持:如Date类型会被转为字符串类型,Undefined和RegExp类型丢失等问题。
无法拷贝存在循环引用的对象。
拷贝自身可枚举字符串属性,原型链丢失。
属性特性丢失。
性能较差。
2.2.2、手动实现深拷贝
同样地,让我们来手动实现深拷贝。
function deepClone(target) {
if (!(target instanceof Object) || 'isClone' in target)
return target;
let clone = null;
if (target instanceof Date)
clone = new target.constructor();
else if(Array.isArray(target))
clone = [];
else
clone = new target.constructor();
let keys = Reflect.ownKeys(target);
for (let key of keys) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
target['isClone'] = null;
clone[key] = deepClone(target[key]);
delete target['isClone'];
}
}
return clone;
}