深入探究深拷贝

169 阅读6分钟

1、深拷贝和浅拷贝的定义

  • ** 浅拷贝:**

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

  • ** 浅拷贝:**

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

2、一行的深拷贝

JSON.parse(JSON.stringify(json));

  • 如果json里面有时间对象,则序列化结果:时间对象=>字符串的形式;
  • 如果json里有RegExp、Error对象,则序列化的结果将只得到空对象 RegExp、Error => {};
  • 如果json里有 function,undefined,则序列化的结果会把 function,undefined 丢失;
  • 如果json里有NaN、Infinity和-Infinity,则序列化的结果会变成null;
  • 如果json里有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor;
  • 如果对象中存在循环引用的情况也无法实现深拷贝

3、基础版

考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,代码如下:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
// 判断是否为引用类型
function isObject(target){
	const type = typeof target;
    retrun target !==null && (type === 'object' || type === 'function');
}
// 初始化数据 
function getInit(target){
	const Ctor = taret.constructor
    return new Ctor()
}
// 实现深拷贝
function clone(target){
	// 如果是原始类型 直接返回
	if(!isObject(target)) return target;
    // 初始化数据类型
    let cloneTarget = getInit(target);
    if(const key in target){
    	cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
}

很简单吧,但是当遇以下测试用例情况,深拷贝的话会出现问题!!!

const target = {
    field1: 1,
    field2: undefined,
    field3: [1,2,3],
    field4: {
        child: 'child',
        child2: {
            child2: 'child2'
        }
    }
};
target.target = target;

如果深拷贝以上代码,由于是循环引用递归就会进入死循环导致栈内存溢出了。

4、解决循环引用

循环引用,既对象的属性间接或者直接的引用了自身的情况,循环引用会因为递归进入死循环导致栈内存溢出。

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储** key-value ** 形式的数据,且** key ** 可以是一个引用类型,我们可以选择** Map或者WeakMap ** 这种数据结构:

Map与WeakMap的对比

  • Map对象的键可以是任何类型,但WeakMap对象中的键只能是对象引用

  • Map是强引用对象,WeakMap是弱引用对象(WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用。)

** 何为弱引用对象:**

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

如果我们要拷贝的对象非常庞大时,使用** Map ** 会对内存造成非常大的额外消耗,而且我们需要手动清除** Map ** 的属性才能释放这块内存,而** WeakMap ** 会帮我们巧妙化解这个问题。

解决循环引用的操作步骤

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为** key ** ,克隆对象作为** value ** 进行存储,继续克隆。
// 判断是否为引用类型
function isObject(target){
	const type = typeof target;
    retrun target !==null && (type === 'object' || type === 'function');
}
// 初始化数据 
function getInit(target){
	const Ctor = taret.constructor
    return new Ctor()
}
// 实现深拷贝
function clone(target, map = new WeakMap()){
	// 如果是原始类型 直接返回
	if(!isObject(target)) return target;
    // 初始化数据类型
    let cloneTarget = getInit(target);
    // 解决循环引用问题
    if(map.get(target)){
    	return map.get(target)
    }
    map.set(target, cloneTarget)
    if(const key in target){
    	cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
}

5、处理其他数据类型

可继续遍历类型

上面我们已经考虑的object、array都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有Map,Set等都是可以继续遍历的类型,这里我们只考虑这四种,

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const arrayTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
cont deepTag = [mapTag,setTag,arrayTag,arrayTag]

// 判断是否为引用类型
function isObject(target){
	const type = typeof target;
    retrun target !==null && (type === 'object' || type === 'function');
}
// 初始化数据 
function getInit(target){
	const Ctor = taret.constructor
    return new Ctor()
}
// 判断类型
function getType(target) {
    return Object.prototype.toString.call(target);
}
// 实现深拷贝
function clone(target, map = new WeakMap()){
	// 如果是原始类型 直接返回
	if(!isObject(target)) return target;
    // 初始化数据类型
    let type = getType(target)
    let cloneTarget
    if(deep includes type){
    	cloneTarget = getInit(target); // 可继续遍历类型
    }
    // 解决循环引用问题
    if(map.get(target)){
    	return map.get(target)
    }
    map.set(target, cloneTarget)
    
    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }
    
    if(const key in target){
    	cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
}

下面我们继续处理其他类型:

不可继续遍历类型

其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理: Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const arrayTag = '[object Object]';
const argsTag = '[object Arguments]';


const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const funcTag = '[object Function]'

cont deepTag = [mapTag,setTag,arrayTag,arrayTag]

// 判断是否为引用类型
function isObject(target){
	const type = typeof target;
    retrun target !==null && (type === 'object' || type === 'function');
}
// 初始化数据 
function getInit(target){
	const Ctor = taret.constructor
    return new Ctor()
}
// 判断类型
function getType(target) {
    return Object.prototype.toString.call(target);
}
// 初始化不可遍历数据类型
function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        case funcTag:
            return cloneFunction(targe);
        default:
            return null;
    }
}
// 克隆Symbol类型:
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}
// 克隆正则:
function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}
// 克隆函数:
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}
// 实现深拷贝
function clone(target, map = new WeakMap()){
	// 如果是原始类型 直接返回
	if(!isObject(target)) return target;
    // 初始化数据类型
    let type = getType(target)
    let cloneTarget
    if(deepTag includes type){
    	cloneTarget = getInit(target); // 可继续遍历类型
    } else {
    	cloneTarget = cloneOtherType(target,type); // 不可继续遍历类型 	
    }
    // 解决循环引用问题
    if(map.get(target)){
    	return map.get(target)
    }
    map.set(target, cloneTarget)
    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }
    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }
    if(const key in target){
    	cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
}

最后 最后

为了更好的阅读,我们用一张图来展示上面所有的代码:

以上整理来源:segmentfault.com/a/119000002…