JS 深拷贝爬坑
JavaSript

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

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

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

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

· slice和concat方法数组拷贝

缺陷:只会对第一层数据进行深拷贝,仅适用于不包含引用对象的一维数组

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);

image.png

...拓展运算符

缺陷:只会对第一层数据进行深拷贝,仅适用于属性不包含引用对象的深拷贝

let obj = { 
	a: 10, 
    b: { 
    	text: '这是原数据'
    }
} 
let obj2 = {...obj}; 
obj2.a = 20;
obj2.b.text = '这是拷贝数据'; 
console.log(obj, obj2);



image.png

Object.assgin 对象合并拷贝

缺陷:只会对第一层数据进行深拷贝,仅适用于属性不包含引用对象的深拷贝

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

下面是重头戏了

·
·
·

Json.parse(Json.stringify())

缺陷有以下几点(具体总结为数据拷贝不完整):

  • 有时间对象,时间对象会变为字符串
  • 有RegExp、Error对象,序列化后变成空对象
  • 有函数,undefined,会丢失属性
  • 有NaN、Infinity 和 -Infinity,序列化后变成null
  • 只能序列化对象的可枚举的自有属性,如果属性是由构造函数生成的,对象变成{}
  • 循环引用无法正确深拷贝(报错: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);

image.png

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


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;
}

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

暂无评论