一文彻底弄懂深拷贝

1,702 阅读8分钟

前言

上个月在部门内部分享了一篇高性能版深拷贝,算是我在字节的第一次技术分享吧。所以这次,准备写一篇正经的深拷贝,毕竟深拷贝实在是太多使用场景和非常高频的面试题了。话不多说,gogogo!!!

漏洞百出的JSON版本

在不引用第三方库又懒得自己手写的情况下,想必JSON版本是大家经常使用的:

JSON.parse(JSON.stringify(target))

这种方式最快速也是最省力的,而且可以应付绝大多数的场景。当然,它依然是漏洞百出的,比如Date类型,undefinedfunction等等,就不多说了。

手写实现深拷贝

当然,我们可以引入lodash,毕竟谁不是一个lodash工程师呢?lodash是一个极其高效有用的工具库,可是如果我们只是需要一个深拷贝就引入一个工具库未免显得有些麻烦(当然可以按需引入),那么,我们就自己去实现一个深拷贝把。

基础版本

不难想到,我们的思路应该是递归拷贝对象中的属性,这样下面的代码就很容易写出来了:

const deepClone = target => {
    const cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    };
    return cloneTarget;
}

我们用下面代码测试一下:

const test = {
    name: 'zhangsan',
    school: {
        name: 'scut',
        address: 'guangzhou'
    }
}

const cloneTest = deepClone(test);

cloneTest.school.name = 'qinghua';

很显然,原数据testschool.name也被改变了,原因很简单,我们在遍历属性的时候,没有判断属性的值是否还是一个对象。

解决深层嵌套版本

这一步的思路依然很简单,判断需要拷贝的数据是不是对象,如果是对象,继续拷贝:

const deepClone = target => {
    if(typeof target === 'object') {
        const cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = deepClone(target[key]);
        };
        return cloneTarget;
    } else {
        return target;
    }
}

兼容数组的版本

除了对象,我们还可能拷贝数组,所以我们需要判断一下拷贝的是数组还是对象,稍微改动即可:

const deepClone = target => {
    if(typeof target === 'object') {
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = deepClone(target[key]);
        };
        return cloneTarget;
    } else {
        return target;
    }
}

也许,上面的兼容数组的版本,就是大家经常熟知的深拷贝版本了,其实,好戏才刚刚开始~~~

兼容其他类型

上面,我们只考虑了数组和对象的拷贝,但是其实还有例如Map,Set,正则,函数等等的深拷贝,我们依然需要去解决。

初始化cloneTarget

因为有很多类型,所以,我们的cloneTarget就不能仅仅判断是不是数组了,那么,我们有没有很简单的方式生成呢,其实是有的,就是拿到引用类型的构造器,然后再new一个相同的引用类型出来就好了。因此,我们增加一个生成cloneTarget的函数:

// 初始化cloneTarget
const initCloneTarget = target => {
    const ctor = target.constructor;
    return new ctor();
}

判断引用类型

我们开始只是通过typeof target === 'object'来简单的判断是否是引用类型,但是还有nullfunction,所以我们需要进一步判断是不是一个引用类型,因此,我们封装一个方法:

// 判断是否是引用类型
const isObject = target => {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

枚举引用类型

接着,需要枚举所有的引用类型,我们需要对一些枚举类型进行特殊处理,例如Date,Set,Symbol等等。我们把枚举类型分为可遍历的和不可遍历的两大类:

// 定义引用类型

// 需要遍历的
const mapTag = '[object Map]';
const setTag = '[object Set]';
const objectTag = '[object Object]';
const arrayTag = '[object Array]';

// 不需要遍历的
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]';

后面,我们需要判断拷贝的数据的类型,然后进行不同处理,所以我们需要一个方法来判断传入的数据类型,当然这个方法极其简单:

// 获取target类型
const getType = target => Object.prototype.toString.call(target);

中场休息

现在,我们停一停,调整一下脑袋,现在的代码如下:

// 定义引用类型

// 需要遍历的
const mapTag = '[object Map]';
const setTag = '[object Set]';
const objectTag = '[object Object]';
const arrayTag = '[object Array]';

// 不需要遍历的
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]';

// 获取target类型
const getType = target => Object.prototype.toString.call(target);


// 初始化cloneTarget
const initCloneTarget = target => {
    const ctor = target.constructor;
    return new ctor();
}

// 判断是否是引用类型
const isObject = target => {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

const deepClone = target => {
    if (isObject(target)) {
        const cloneTarget = initCloneTarget(target);
        for (const key in target) {
            cloneTarget[key] = deepClone(target[key]);
        };
        return cloneTarget;
    } else {
        return target
    }
}

OK,休息好了么?我们继续!

改造主方法

通过上面,我们知道除了对象和数组,还有set,map,functionSymbol等等都会被isObject判断为真,所以,这个时候在for..in去拷贝就不合适了,因为我们需要区分处理。

首先,我们增加一个方法,判断是不是可遍历的引用类型:

// 是否可遍历
const isInterator = type => [mapTag, setTag, objectTag, arrayTag].includes(type);

接着,如果不是引用类型,我们直接返回,否则,我们初始化cloneTarget,然后根据每个类型一一处理:

const deepClone = target => {
    // 原始类型直接返回
    if(!isObject(target)) {
        return target;
    }
    let cloneTarget;
    const type = getType(target);
    if (isInterator(type)) {
        // 如果是可遍历类型,初始化cloneTarget
        cloneTarget = initCloneTarget(target);
    } else {
        // 不是可遍历类型,单独处理返回
        return deepCloneOther(target, type);
    };
    // ...
}

我们暂时不处理deepCloneOther方法,先看看可遍历的四种类型该怎么处理。

处理Set

set的处理很简单,只需要遍历target里面的数据,然后往cloneTarget里面添加即可,当然,拿到的数据依然需要递归拷贝。

// 处理set
if(type === setTag) {
    target.forEach(item => {
        cloneTarget.add(deepClone(item))
    });
    return cloneTarget;
};

处理Map

Map的处理和Set几乎一致,就不多说了:

// 处理map
if (type === mapTag) {
    target.forEach((value, key) => {
        cloneTarget.set(key, deepClone(value))
    });
    return cloneTarget;
}

处理对象和数组

对象和数组就和最开始一样处理即可,循环属性然后遍历赋值即可:

// object 和 array
for (const key in target) {
    cloneTarget[key] = deepClone(target[key]);
};
return cloneTarget;

处理寻常引用类型

对于无需遍历的类型,除了RegexpfunctionSymbol需要特殊处理,其他类型的只需要new一个同样的引用类型即可,所以我们自然而然这样写:

// 处理不可遍历类型
const deepCloneOther = (target, type) => {
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            const ctor = target.constructor;
            return new ctor(target);
            break;
    
        default:
            break;
    }
}

处理正则

正则并不是这一次的主要内容,正则处理的原理也特别简单,只是我们需要对lastIndex进行处理一下:

// 处理正则
const cloneRegExp = target => {
    const result = new target.constructor(target.source, /\w*$/.exec(target));
    result.lastIndex = target.lastIndex;
    return result;
}

处理Symbol

处理Symbol也没什么说的,如下:

// 处理Symbol
const cloneSymbol = target => {
    const symbolValueOf = Symbol.prototype.valueOf;
    return Object(symbolValueOf.call(target));
}

处理function

实际上,clone一个函数是没有任何意义的也是没有任何场景的,所以在lodash中只是直接返回了一个{}或者本身函数。那么,我们也直接返回本身把:

// 处理函数
const cloneFunction = target => {
    return target || {};
}

补全deepCloneOther

现在,不可遍历的类型也处理完了,我们就加入到deepCloneOther方法中:

// 处理不可遍历类型
const deepCloneOther = (target, type) => {
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            const ctor = target.constructor;
            return new ctor(target);
            break;
        case regexpTag:
            return cloneRegExp(target);
            break;
        case symbolTag:
            return cloneSymbol(target);
            break;
        case funcTag:
            return cloneFunction(target);
            break;
        default:
            break;
    }
}

那么,到现在,兼容数据类型就做完了,到目前为止的代码如下:

// 定义引用类型

// 需要遍历的
const mapTag = '[object Map]';
const setTag = '[object Set]';
const objectTag = '[object Object]';
const arrayTag = '[object Array]';

// 不需要遍历的
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]';

// 获取target类型
const getType = target => Object.prototype.toString.call(target);

// 是否可遍历
const isInterator = type => [mapTag, setTag, objectTag, arrayTag].includes(type);


// 初始化cloneTarget
const initCloneTarget = target => {
    const ctor = target.constructor;
    return new ctor();
}

// 判断是否是引用类型
const isObject = target => {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

// 处理正则
const cloneRegExp = target => {
    const result = new target.constructor(target.source, /\w*$/.exec(target));
    result.lastIndex = target.lastIndex;
    return result;
}

// 处理Symbol
const cloneSymbol = target => {
    const symbolValueOf = Symbol.prototype.valueOf;
    return Object(symbolValueOf.call(target));
}

// 处理函数
const cloneFunction = target => {
    return target || {};
}

// 处理不可遍历类型
const deepCloneOther = (target, type) => {
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            const ctor = target.constructor;
            return new ctor(target);
            break;
        case regexpTag:
            return cloneRegExp(target);
            break;
        case symbolTag:
            return cloneSymbol(target);
            break;
        case funcTag:
            return cloneFunction(target);
            break;
        default:
            break;
    }
}

const deepClone = target => {
    // 原始类型直接返回
    if (!isObject(target)) {
        return target;
    }
    let cloneTarget;
    const type = getType(target);
    if (isInterator(type)) {
        // 如果是可遍历类型,初始化cloneTarget
        cloneTarget = initCloneTarget(target);
    } else {
        // 不是可遍历类型,单独处理返回
        return deepCloneOther(target, type);
    };
    
    // 处理set
    if (type === setTag) {
        target.forEach(item => {
            cloneTarget.add(deepClone(item))
        });
        return cloneTarget;
    };

    // 处理map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, deepClone(value))
        });
        return cloneTarget;
    };

    // object 和 array
    for (const key in target) {
        cloneTarget[key] = deepClone(target[key]);
    };
    return cloneTarget;
}

到现在,我们可以测试一下,提供一个对象供大家测试:

const set = new Set();
set.add({
    staffId: 1,
    staffName: '赵云'
});
set.add('haah');

const map = new Map();
map.set('key1', {
    staffId: 2,
    staffName: '关羽'
});

const test = {
    name: 'zhangsan',
    school: {
        name: 'scut',
        address: 'guangzhou'
    },
    set,
    map,
    list: [{
        staffId: '4',
        staffName: '刘备'
    }],
    boolean: new Boolean(true),
    error: new Error(),
    date: new Date(),
    number: new Number(1),
    regexp: /\w+/,
    string: new String('zhangsan'),
    symbol: Symbol(1),
    function: () => {
        console.log(123)
    }
}

解决循环引用

那么到现在为止,就大功告成了么,其实并没有,看下面的对象:

const test = {
    name: 'zhangsan'
};

test.target = test;

emem......,栈溢出了....

所以,我们还需要解决循环引用的问题,解决思路是:我们可以开辟一块空间,来当做一个数据池,用来存储拷贝过的对象,每次拷贝的时候,就去数据池中找,如果找到了,就直接拿来用,否则就走以前的拷贝逻辑。添加代码如下:

// 解决循环引用
if(map.get(target)) {
    return target;
}
map.set(target, cloneTarget);

那么,现在就可以解决循环引用的问题了。

最后

结束,撒花~~~ 附上完整代码:

// 定义引用类型

// 需要遍历的
const mapTag = '[object Map]';
const setTag = '[object Set]';
const objectTag = '[object Object]';
const arrayTag = '[object Array]';

// 不需要遍历的
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]';

// 获取target类型
const getType = target => Object.prototype.toString.call(target);

// 是否可遍历
const isInterator = type => [mapTag, setTag, objectTag, arrayTag].includes(type);


// 初始化cloneTarget
const initCloneTarget = target => {
    const ctor = target.constructor;
    return new ctor();
}

// 判断是否是引用类型
const isObject = target => {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

// 处理正则
const cloneRegExp = target => {
    const result = new target.constructor(target.source, /\w*$/.exec(target));
    result.lastIndex = target.lastIndex;
    return result;
}

// 处理Symbol
const cloneSymbol = target => {
    const symbolValueOf = Symbol.prototype.valueOf;
    return Object(symbolValueOf.call(target));
}

// 处理函数
const cloneFunction = target => {
    return target || {};
}

// 处理不可遍历类型
const deepCloneOther = (target, type) => {
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            const ctor = target.constructor;
            return new ctor(target);
            break;
        case regexpTag:
            return cloneRegExp(target);
            break;
        case symbolTag:
            return cloneSymbol(target);
            break;
        case funcTag:
            return cloneFunction(target);
            break;
        default:
            break;
    }
}

const deepClone = (target, map = new Map()) => {
    // 原始类型直接返回
    if (!isObject(target)) {
        return target;
    }
    let cloneTarget;
    const type = getType(target);
    if (isInterator(type)) {
        // 如果是可遍历类型,初始化cloneTarget
        cloneTarget = initCloneTarget(target);
    } else {
        // 不是可遍历类型,单独处理返回
        return deepCloneOther(target, type);
    };

    // 解决循环引用
    if(map.get(target)) {
        return target;
    }
    map.set(target, cloneTarget);
    
    // 处理set
    if (type === setTag) {
        target.forEach(item => {
            cloneTarget.add(deepClone(item, map))
        });
        return cloneTarget;
    };

    // 处理map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, deepClone(value, map))
        });
        return cloneTarget;
    };

    // object 和 array
    for (const key in target) {
        cloneTarget[key] = deepClone(target[key], map);
    };
    return cloneTarget;
}

那么,你学会了么?快去撸起来吧!!!