实现简单的深拷贝

108 阅读3分钟

深拷贝

我们知道在 JS 中,数据类型分为简单类型和引用类型,也就会出现引用的问题,那么我们想要创建一个对象的副本,不能简单地用另一个变量指向它,由于指向同一块内存地址,本质上还是浅拷贝

首先要做类型的判断

const deepClone=(x)=>{
    if(x instanceof Object){
        if(x instanceof Array){}
        else if(x instanceof Function){}
        else if(x instanceof Date){}
        else if(x instanceof RegExp){}
        else{}
    }else{
        return x
    }
}

首先我们要对数据类型的判断很熟悉,不用 typeof 的原因是,typeof 判断 null 为 ‘object’,而且对于数组和普通对象无法区分,而 instanceof 利用原型链判断,比较精确

接下来就添加具体的拷贝方法

const deepClone = (x) => {
    if (x instanceof Object) {
        let result
        if (x instanceof Array) {
            result = new Array(x.length)
        } else if (x instanceof Function) {
            if (x.prototype) {
                result = function (arg) { return x.call(this, arg) }
            } else {
                result = (arg) => { return x(arg)  }
            }
        } else if (x instanceof Date) {
            result = new Date(x - 0)
        } else if (x instanceof RegExp) {
            result = new RegExp(x.source, x.flags)
        } else {
            result = {}
        }
        for (let key in x) {
            result[key] = deepClone(x[key])
        }
        return result
    } else {
        return x
    }
}

这里只对普通函数和箭头函数进行处理,可以取其 prototype 来判断

对时间戳对象减去 0,可获得毫秒数,然后构造新实例

需要对对象进行递归处理,防止对象出现嵌套情况而拷贝失败

看起来没问题了,我们来试一下

const a = {
    name: 'frank',
    age: 18,
    live: true,
    color: null,
    friend: undefined,
    f1: function () {},
    f2: () => {},
    now: new Date(),
    match: '/^replace',
    info: {
        name: 'mike',
        age: 20
    }
}
const b = deepClone(a)

b.png 可以看到,我们成功的拷贝了 a 的副本,即使修改 b,也不会影响 a 的值

b1.png 当我们认为大功告成的时候,来观察一个现象

window.png window 对象居然自己引用自己,我们来试下对源对象实现环引用会怎样

const a = {
    name: 'frank',
    age: 18,
    live: true,
    color: null,
    friend: undefined,
    f1: function () {},
    f2: () => {},
    now: new Date(),
    match: '/^replace',
    info: {
        name: 'mike',
        age: 20
    }
}
a.self=a
const b = deepClone(a)

max.png 可以看到,由于我们使用了递归和环引用,浏览器会无限制调用函数,直到充满调用栈

所以我们应该记录下来已经拷贝过的属性,以免无尽调用,最好使用 Map 或 WeakMap 来记录,因为键值有可能是对象,普通对象无法满足需求

const cache=new Map()
const deepClone = (x) => {
    if (x instanceof Object) {
    if (cache.get(x)) { return cache.get(x) }
        let result
        if (x instanceof Array) {
            result = new Array(x.length)
        } else if (x instanceof Function) {
            if (x.prototype) {
                result = function (arg) { return x.call(this, arg) }
            } else {
                result = (arg) => { return x(arg)  }
            }
        } else if (x instanceof Date) {
            result = new Date(x - 0)
        } else if (x instanceof RegExp) {
            result = new RegExp(x.source, x.flags)
        } else {
            result = {}
        }
        cache.set(x,result)
        for (let key in x) {
            result[key] = deepClone(x[key])
        }
        return result
    } else {
        return x
    }
}

测试结果如下

self.png 用 Map 设置了递归出口,防止环引用带来的无限递归

目前方法还有一个缺点,就是只能使用一次,因为 Map 中的值会累积进而影响下次拷贝,可以进行改造

const deepClone = (x,cache) => {
    if(!cache){
        cache=new Map()
    }
    if (x instanceof Object) {
        if (cache.get(x)) { return cache.get(x) }
        let result
        if (x instanceof Array) {
            result = new Array(x.length)
        } else if (x instanceof Function) {
            if (x.prototype) {
                result = function (arg) {
                    return x.call(this, arg)
                }
            } else {
                result = (arg) => {
                    return x(arg)
                }
            }
        } else if (x instanceof Date) {
            result = new Date(x - 0)
        } else if (x instanceof RegExp) {
            result = new RegExp(x.source, x.flags)
        } else {
            result = {}
        }
        cache.set(x, result)
        for (let key in x) {
            if(x.hasOwnProperty(key)){
                result[key] = deepClone(x[key],cache)
            }
        }
        return result
    } else {
        return x
    }
}

利用函数参数来传递 Map,每一次拷贝都维护各自的参数

拷贝时对属性进行判断,不拷贝继承属性