前端架构系列

深拷贝和浅拷贝

2020-06-26  本文已影响0人  羽晞yose

借用网上基本为复制粘贴的一段话:

深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用,深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变

总结上面的话就是:
深拷贝:新开堆内存,对原数据不会产生影响
浅拷贝:复制空间引用地址,栈内存中操作,会对原数据产生影响

深拷贝(常用数组与对象拷贝方法汇总)

let arr = [1, 2, {
    text: '这是源数据'
}];
let arr2 = arr.slice();
let arr3 = arr.concat([]);

arr2[1] = 20;
arr2[2].text = '这是拷贝数据';

arr3[1] = 30;
arr3[2].text = '这是拷贝数据3';
console.log(arr, arr2, arr3);
slice拷贝
concat拷贝
let obj = {
    a: 10,
    b: {
        text: '这是原数据'
    }
}

let obj2 = {...obj};

obj2.a = 20;
obj2.b.text = '这是拷贝数据';

console.log(obj, obj2);
拓展运算符拷贝
上图输出结果可以看出,属性a的值复制的是实例,而属性b复制的是空间地址,所以更改obj2的时候,obj.b也被改变了
let obj = {
    a: 10,
    b: {
        text: '这是原数据'
    }
}

let obj2 = Object.assign({}, obj); // 注意第一个参数是source,也就是源对象,第二个为要合并进去的对象,顺序如果颠倒会变成直接更改obj

obj2.a = 20;
obj2.b.text = '这是拷贝数据';

console.log(obj, obj2);
image.png
  1. 有时间对象,时间对象会变为字符串
  2. 有RegExp、Error对象,序列化后变成空对象
  3. 有函数,undefined,会丢失属性
  4. 有NaN、Infinity 和 -Infinity,序列化后变成null
  5. 只能序列化对象的可枚举的自有属性,如果属性是由构造函数生成的,对象变成{}
  6. 循环引用无法正确深拷贝(报错:Converting circular structure to JSON)
// 没完整覆盖上面所有情况,但每一点都写到里面了,除了递归引用,会导致程序没法正确执行,自行放开注释即可
class Test {}
let test = new Test();

// let loopObj = {}
// loopObj.a = loopObj; // 递归引用

let obj = {
    a: 10,
    b: {
        text: '这是原始数据',
        val: 20
    },
    c: function () {},
    d: undefined, // 虽然这个确实扯,给一个属性赋值undefined…
    e: new Date(),
    f: /A/,
    h: NaN,
    i: test, // 丢失constructor
    // j: loopObj
}

let obj2 = JSON.parse(JSON.stringify(obj)); // Converting circular structure to JSON

obj2.a = 100;
obj2.b.text = '这是拷贝数据';

console.log(obj, obj2);

Json.parse(Json.stringify())拷贝

因此想完整的实现深拷贝,需要使用递归来拷贝

function deepClone (obj, hash = new WeakMap()) {
    // 因为null == undefined,所以 null 与 undefined 直接返回
    // 如果是dom元素或基本类型值,直接返回
    const isElement = obj instanceof Element;
    const isEmpty = obj == undefined;
    const isNormalType = typeof obj !== 'object';

    if (isElement || isEmpty || isNormalType) return obj;

    // Object.prototype.toString.call,但window本身就是一个对象,所以直接toString.call()也是一样的
    // 当然这样写有风险,当某天window的toString被改写了,那程序就会出问题了,写文章还是严谨点
    let type = Object.prototype.toString.call(obj);
    
    switch (type) {
        case '[object Date]': return new Date(obj);
        case '[object RegExp]': return new RegExp(obj);
        case '[object Error]': return Error(obj);  // Error 与 new Error是一致的
    }

    if (hash.get(obj)) return obj;

    // 否则证明不是数组就是对象,需要递归拷贝
    // constructor保存着原函数的引用,因此直接不需要判断是数组还是对象来生成
    let clone = new obj.constructor;
    // 存储到weakMap中,下一次进来,先查找是否已有该值,有的话直接返回即可,用于解决循环引用
    hash.set(obj, clone);
    for (let key in obj) {
        clone[key] = deepClone(obj[key], hash);
    }
    
    return clone;
}
自行实现的完整克隆

关于上面的日期、正则等,其实直接返回也是可以的,因为没法更改源数据,要改那肯定是重新实例一个新的对象出来。

上一篇下一篇

猜你喜欢

热点阅读