关于js深拷贝和浅拷贝

山水有轻音 2020-10-15 AM 533℃ 0条

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;
}
标签: none

非特殊说明,本博所有文章均为博主原创。

评论啦~