对象深浅拷贝

141 阅读5分钟

浅拷贝

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

1322857557-5d6c4f59de0f1.jpg

深拷贝

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

1764790084-5d6c4f595afe0.jpg

乞丐版实现方式

JSON.parse(JSON.stringify(obj))

我们想要深拷贝一个对象,使用最多的方法应该就是这个了,写法非常简单,而且可以应对大部分应用场景,但是它有个很大的缺陷,比如拷贝其他引用类型、拷贝函数、循环引用等。

基础版本

function clone(target){
    let cloneTarget={}
    for(const key in target){
        cloneTarget[key]=target[key]
    }
    return cloneTarget
}

创建一个新对象,遍历需要克隆的对象,将需要克隆对象的属性一次添加到新对象上,返回新对象。

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

function clone(target) {
    if (typeof target !== 'object') {
        return target
    } else {
        let cloneTarget = {}
        for (const key in target) {
            cloneTarget[key] = clone(target[key])
        }
        return cloneTarget
    }
}

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是还是有很多缺陷,比如没有考虑到数组。

考虑数组

function clone(target) {
    if (typeof target !== 'object') {
        return target
    } else {
        let cloneTarget = Array.isArray(target)?[]:{}
        for (const key in target) {
            cloneTarget[key] = clone(target[key])
        }
        return cloneTarget
    }
}

循环引用问题

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

如果使用上面函数去直接该对象,会直接报错,原因很明显,因为递归进入了死循环导致栈内存溢出。

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

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

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function clone(target,map=new WeakMap()) {
    if (typeof target !== 'object') {
        return target
    } else {
        let cloneTarget = Array.isArray(target)?[]:{}
        if(map.get(target)){
            return map.get(target)
        }
        map.set(target,cloneTarget)
        for (const key in target) {
            cloneTarget[key] = clone(target[key],map)
        }
        return cloneTarget
    }
}

解释下这里为什么使用WeakMap而不是事用Map的原因。

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

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

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的。

function clone(target, map = new WeakMap()) {
    if (typeof target !== 'object') {
        return target
    } else {
        let cloneTarget = Array.isArray(target) ? [] : {}
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget)
        Object.keys(target).forEach(key => {
            cloneTarget[key] = clone(target[key], map)
        })
        return cloneTarget
    }
}

其他数据类型

首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
获取数据类型

我们可以使用toString来获取准确的引用类型

function getType(target) {
    return Object.prototype.toString.call(target);
}

4096785459-5d6c4f585745d.jpg 抽离出一些常用的数据类型方便后面使用

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[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]';

以上类型中,可以简单分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型
可继续遍历的类型

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

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们该写clone函数,对可继续遍历的数据类型进行处理

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
    return Object.prototype.toString.call(target);
}

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[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]';
const deepTag = [mapTag, setTag, arrayTag, objectTag]
function clone(target, map = new WeakMap()) {
    if (!isObject(target)) {
        return target
    }
    const type = getType(target)
    let cloneTarget
    if(deepTag.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
    }
    //克隆对象数组
    Object.keys(target).forEach(key => {
        cloneTarget[key] = clone(target[key], map)
    })
    return cloneTarget

}
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    field5:new Set([1,2,3]),
    field6:new Map([[111,222],[222,333]])
};
target.target = target;

const obj = clone(target)
console.log(obj)

不可继续遍历的类型

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

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);
        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 isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
//获取数据类型
function getType(target) {
    return Object.prototype.toString.call(target);
}
//初始化数据
function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}
//克隆其他数据类型
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);
    }
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[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]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag]

function clone(target, map = new WeakMap()) {
    if (!isObject(target)) {
        return target
    }
    const type = getType(target)
    let cloneTarget
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target)
    } else {
        return 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
    }
    //克隆对象数组
    Object.keys(target).forEach(key => {
        cloneTarget[key] = clone(target[key], map)
    })
    return cloneTarget

}
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    field5: new Set([1, 2, 3]),
    field6: new Map([
        [111, 222],
        [222, 333]
    ]),
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};
target.target = target;

const obj = clone(target)
console.log(obj)