乐趣区

前端基础深拷贝

1. 背景:为什么有必要掌握深拷贝?

在业务中经常会遇到需要对模板数据 / 初始数据进行加工处理,而且,该数据可能能需要在多处被复用。

简单粗暴更改原始数据的做法,会污染其他依赖项,所以我们需要对原始数据进行一份深拷贝,保持各个依赖项数据的独立性,做到既可以复用,又不互相污染。

2. JS 中没有自带的深拷贝 API

也许我们用到过 Object.assign() 进行对象合并,亦或者用数组的 slice(), concat() 方法对数组进行复制,但这几种方法都不是深拷贝,是浅拷贝。

简言之,深拷贝就是每个对象都是独立的,独立意味着拥有各自的独立内存空间;而浅拷贝意味着拥有公共的引用空间。

所以,值类型的数据不存在浅拷贝的问题,只有引用数据类型才存在。愿更深入的了解深拷贝和浅拷贝的,可以参考文章最末尾的「参考文章」。

3. 对于数组的深拷贝,一个很简单的做法:

let recursiveClone = val => Array.isArray(val) ? Array.from(val, recursiveClone) : val;

4. 封装:对于任意一个 JS 数据类型的深拷贝

概述 JS 的数据类型,从大致上分为两种:

  1. 基本数据类型:string, number, boolean, undefined, null, symbol
  2. 引用数据类型: object(这是一个统称,包含 object, array, function 以及 js 单体内置对象如 Date, RegExp, Error 等)

对于基本数据类型,不存在深拷贝的问题,因为它们是值类型的数据。值类型的数据存放在栈内存中,重新赋值就是独立的。

而对于众多的引用数据类型,需要分别进行处理,集中处理 object 和 array,这也是我们在业务中遇到最多的情况。

5. 上代码:

const deepCloneTypes = ['Object', 'Array', 'Map', 'Set'];

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

function getType(source) {return (Object.prototype.toString.call(source)).split(' ')[1].slice(0, -1)
}

function processOtherType(source) {
    const Ctor = source.constructor;
    return new Ctor(source);
}

function processFunctionType (source) {let _source = source.toString();
    // 区分是否是箭头函数
    if (source.prototype) {
        // 如果有 prototype 就是普通函数
        let argsReg = /function\s*\w*\(([^\)]*)\)/;
        let bodyReg = /\{([\s\S]*)\}/;
        let fnArgs = (argsReg.exec(source))[1];
        let fnBody = (bodyReg.exec(source))[1];
        console.log(fnArgs, fnBody);
        return new Function (fnArgs, fnBody)
    } else {
        // 箭头函数没有 prototype
        return eval(_source)
    }
}

function deepClone (source, map = new WeakMap()) {
    // 首先用 typeof 来筛选是否是引用数据类型,如果连引用数据类型都不是,那么就判断是基本数据类型,直接返回即可
    if (!isObject(source)) {return source}

    const type = getType(source);
    let cloneTarget;

    // 防止循环引用
    if (map.get(source)) {return map.get(source);
    }
    map.set(source, cloneTarget);

    // 接下来判断是否是需要进行循环拷贝的引用数据类型,诸如 new Boolean, new Number 这样的,也不需要循环拷贝
    if (!deepCloneTypes.includes(type)) {cloneTarget = processOtherType(source);
    } else {cloneTarget = new source.constructor();
        // return Object.create(source.constructor.prototype) // 不能这样,这样是创造了一个对象

        if (type === 'Object' || type === 'Array') {let keys = type === 'Object' ? Object.keys(source) : undefined; // 如果支持 optional chaining 的话可以写成?.
            (keys || source).forEach((val, key) => {if (keys) {key = val;}
                cloneTarget[key] = deepClone(source[key], map); // 在这里进行递归调用
            })
        }
    
        if (type === 'Function') {cloneTarget = processFunctionType(source)
        }

        if (type === 'Map') {source.forEach((val, key) => {cloneTarget.set(key, deepClone(val, map))
            })
        }

        if (type === 'Set') {source.forEach((val, key) => {cloneTarget.add(deepClone(val, map))
            })
        }
    }

    return cloneTarget
}

参考:

如何写出一个惊艳面试官的深拷贝?

JavaScript 深入了解基本类型和引用类型的值

Array.from() 五个超好用的用途

退出移动版