JS基础 - 手写深拷贝

参考:https://juejin.im/post/6844903929705136141#heading-9

 

提要

  1. 基本实现:递归,数组与对象
  2. 处理循环引用:使用map记录已克隆过的对象
  3. 克隆更多的类型:可遍历类型(Array/Set/Map/Object); 不可遍历类型(String/Number/Symbol/RegExp)
    =>如何获取引用类型的具体类型;通用的引用类型初始化函数;为什么要将Symbol转换为Object;正则表达式的构造函数
  4. 克隆函数:普通函数;箭头函数
    =>  如何区分普通函数和箭头函数;函数体和参数的正则表达式;参数为空的考虑

基础版

思路:深拷贝一个对象(数组),就是要深拷贝对象(数组)中的每一个元素,很显然应当用递归实现。

  • 递归退出条件:当传入的变量是基本类型,或为null时,直接返回变量本身。
  • 递推工作:深拷贝每一个属性,注意用hasOwnProperty保证只拷贝自身的属性,不拷贝继承到的属性。

代码:

function deepClone(obj){
    //不是引用类型或为null时退出递归
    if(typeof obj !== "object" || obj == null){
        return obj;
    }

    //初始化新变量
    let copy;
    if(obj instanceof Array){
        copy = [];
    }else{
        copy = {};
    }

    //深拷贝每一个元素
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            copy[key] = deepClone(obj[key]);
        }
    }

    return copy;
}

 

循环引用

如果一个对象的属性引用了自身,上述代码会使程序无限递归。我们可以使用一个map,记录已经拷贝过的所有对象,在拷贝时,首先检查map中是否存在该对象,若有,则直接返回,否则,将当前对象记录到map中,key为其本身,value为其克隆体,然后继续正常的克隆。

function deepClone(obj, map = new Map()){
    //不是引用类型或为null时退出递归
    if(typeof obj !== "object" || obj == null){
        return obj;
    }

    //检查是否已经克隆过
    if(map.get(obj)){
        return map.get(obj);
    }

    //初始化新变量
    let copy;
    if(obj instanceof Array){
        copy = [];
    }else{
        copy = {};
    }
    //加入map
    map.set(obj, copy);

    //深拷贝每一个元素
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            copy[key] = deepClone(obj[key], map);
        }
    }

    return copy;
}

 

考虑更多的数据类型

首先,使用toString来获取引用类型具体的类型:

每一个引用类型都有toString()方法,如果该方法没有被重写过,则返回“[object type]”,其中type是对象的具体类型。

由于大部分引用类型都重写了toString(),我们需要直接调用Object原型上的toString()

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

我们可以大致将各种引用类型分为两类:需要递归拷贝的,如Array/Set/Map;不需要递归拷贝的,如Number/Boolean/String

首先看需要递归拷贝的,我们需要获取他们的初始化数据,即获取其构造函数,然后创建一个空对象并返回:

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

可以克隆set和map的完整方法:

const mapTag = getType(new Map());
const setTag = getType(new Set());
const arrayTag = getType(new Array());
const objectTag = getType(new Object());

//判断是否需要递归拷贝,即是非null的引用类型
function isObject(variable){
    return (variable != null) && (typeof variable == "object" || typeof variable == "function");
}

//通过toString获取具体变量类型
function getType(obj){
    return Object.prototype.toString.call(obj);
}

//获取构造函数并返回空对象
function getInit(obj){
    const Constructor = obj.constructor;
    return new Constructor();
}

function deepClone(obj, map = new Map()){
    //不需要递归拷贝,直接克隆值
    if(!isObject(obj)){
        return obj;
    }

    //防止循环引用
    if(map.get(obj)){
        return map.get(obj);
    }

    //初始化新变量
    const type = getType(obj);
    let copy = getInit(obj);

    map.set(obj, copy);

    //克隆对象和数组
    if(type === arrayTag || type === objectTag){
        for(let key in obj){
            if(obj.hasOwnProperty(key)){
                copy[key] = deepClone(obj[key], map);
            }
        }
    }
    
    //克隆map,value也需要深拷贝
    else if(type === mapTag){
        obj.forEach((val, key) => {
            copy.set(key, deepClone(val, map))
        });
    }

    //克隆set,深拷贝每一个元素
    else if(type === setTag){
        obj.forEach(val => {
            copy.add(deepClone(val, map))
        })
    }
    
    return copy;
}

 

继续考虑其他引用类型,这里只考虑Boolean, Number, String, Symbol 和 RegExp。

对于Symbol,由于一个程序中不能存在两个等值的Symbol,我们只能将其拷贝成一个Object:

function cloneSymbol(obj) {
    return Object(Symbol.prototype.valueOf.call(obj));
}

克隆正则:

function cloneReg(obj) {
    const reFlags = /\w*$/; //匹配在字符串末尾出现的0个或多个字母、数字、下划线,用于找修饰符
    const copy = new RegExp(obj.source, reFlags.exec(obj));
    copy.lastIndex = obj.lastIndex; //同步lastIndex

    return copy;
}

Boolean, Number, String可以直接用构造函数克隆,合起来:

function cloneOtherTypes(obj, type){
    const Constructor = obj.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
            return new Constructor(obj);
        case symbolTag:
            return cloneSymbol(obj);
        case regTag:
            return cloneReg(obj);
        default:
            return null;
    }
}

 

克隆函数

这里只简单的取出函数参数和函数体然后用new Function克隆。

匹配函数体的正则:/(?<={)(.|\n)*(?=})/m         "{" 以后 "}" 以前 的 非换行符 或 换行符 ,多行匹配

匹配参数列表的正则:/(?<=\().+(?=\)\s*{)/      "(" 以后 ") }" 以前 的 非换行符

1. 使用toString将函数转化为字符串

2. 通过prototype判断是否为普通函数

3. 对普通函数,若有参数,首先将参数列表字符串转化成数组paramArray,然后调用new Function(...paramArr, body[0])生成新函数;

若没有参数,return new Function(body[0]);

4. 对箭头函数,return eval(funcString);

function cloneFunc(obj){
    const bodyReg = /(?<={)(.|\n)*(?=})/m;
    const paramReg = /(?<=\()(.|\n)*(?=\)\s*{)/m;
    const funcString = obj.toString();
    if(obj.prototype){
        //普通函数
        console.log('普通函数');
        const body = bodyReg.exec(funcString);
        const param = paramReg.exec(funcString);
        if(param){
            const paramArray = param[0].split(',');
            console.log('匹配到参数:', paramArray);
            console.log('body:', body[0]);
            return new Function(...paramArray, body[0]);
        }else{
            return new Function(body[0]);
        }
    } else { //箭头函数
        return eval(funcString);
    }

}

 

最后,将所有函数合在一起:

//将各个引用类型名存储为常量,以便后续使用
const mapTag = getType(new Map());
const setTag = getType(new Set());
const arrayTag = getType(new Array());
const objectTag = getType(new Object());

const boolTag = getType(true);
const stringTag = getType("1");
const numberTag = getType(1);

const symbolTag = getType(Symbol());
const regTag = getType(new RegExp());

const deepTags = [mapTag, setTag, arrayTag, objectTag];



//判断是否需要递归拷贝,即是非null的引用类型
function isObject(variable){
    return (variable != null) && (typeof variable == "object" || typeof variable == "function");
}

//通过toString获取具体变量类型
function getType(obj){
    return Object.prototype.toString.call(obj);
}

//获取构造函数并返回空对象
function getInit(obj){
    const Constructor = obj.constructor;
    return new Constructor();
}

//克隆Bool, Number, String, Symbol
function cloneOtherTypes(obj, type){
    const Constructor = obj.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
            return new Constructor(obj);
        case symbolTag:
            return cloneSymbol(obj);
        case regTag:
            return cloneReg(obj);
        default:
            return null;
    }
}

//克隆Symbol
function cloneSymbol(obj) {
    return Object(Symbol.prototype.valueOf.call(obj));
}

//克隆正则
function cloneReg(obj) {
    const reFlags = /\w*$/; //匹配在字符串末尾出现的0个或多个字母、数字、下划线,用于找修饰符
    const copy = new RegExp(obj.source, reFlags.exec(obj));
    copy.lastIndex = obj.lastIndex; //同步lastIndex

    return copy;
}

//克隆函数
function cloneFunc(obj){
    const bodyReg = /(?<={)(.|\n)*(?=})/m;
    const paramReg = /(?<=\()(.|\n)*(?=\)\s*{)/m;
    const funcString = obj.toString();
    if(obj.prototype){
        //普通函数
        console.log('普通函数');
        const body = bodyReg.exec(funcString);
        const param = paramReg.exec(funcString);
        if(param){
            const paramArray = param[0].split(',');
            console.log('匹配到参数:', paramArray);
            console.log('body:', body[0]);
            return new Function(...paramArray, body[0]);
        }else{
            return new Function(body[0]);
        }
    } else { //箭头函数
        return eval(funcString);
    }

}



function deepClone(obj, map = new Map()){
    //不需要递归拷贝,直接克隆值
    if(!isObject(obj)){
        return obj;
    }

    //防止循环引用
    if(map.get(obj)){
        return map.get(obj);
    }

    //初始化新变量
    const type = getType(obj);
    let copy;
    if(deepTags.includes(type)){
        copy = getInit(obj);
    }
    
    //记录已经克隆过的对象
    map.set(obj, copy);

    //克隆对象和数组
    if(type === arrayTag || type === objectTag){
        for(let key in obj){
            if(obj.hasOwnProperty(key)){
                copy[key] = deepClone(obj[key], map);
            }
        }
    }

    //克隆函数
    else if(typeof obj === "function"){
       copy = cloneFunc(obj);
        
    }
    
    //克隆map,value也需要深拷贝
    else if(type === mapTag){
        obj.forEach((val, key) => {
            copy.set(key, deepClone(val, map))
        });
    }

    //克隆set,深拷贝每一个元素
    else if(type === setTag){
        obj.forEach(val => {
            copy.add(deepClone(val, map))
        })
    }
    
    //克隆其他类型
    else{
        copy = cloneOtherTypes(obj, type);
    }

    return copy;
}


const map = new Map();
map.set('key', 'value');
map.set('ConardLi', 'code秘密花园');

const set = new Set();
set.add('ConardLi');
set.add('code秘密花园');

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    reg: /\d+/,
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};

console.log(deepClone(target));

  

 

posted @ 2020-06-27 07:18  studystudyxinxin  阅读(694)  评论(0)    收藏  举报