JavaScript深拷贝的一些坑

193 阅读4分钟
原文链接: click.aliyun.com

前言

之前去一家公司面试的时候,面试官问了我一个问题,说:"如何才能深拷贝一个对象"。当时我心里有些窃喜,这么简单的问题还用想吗?于是脱口而出:"平时常用的有两种办法,第一种用JSON.parse(JSON.stringify(obj)),第二种可以使用for...in加递归完成"。面试官听了以后点了点头觉得挺满意的。 当时我也并没有太过在乎这个问题,直到前段时间又想起这个问题,发现上面说的两种方法都是有Bug的。

提出问题

那么上面所说的Bug是什么呢?

  • 特殊对象拷贝

首先让我们试想有这么一个对象,在不考虑普通类型的情况下,它有如下成员:




const obj = {
 arr: [111, 222],
 obj: {key: '对象'},
 a: () => {console.log('函数')},
 date: new Date(),
 reg: /正则/ig
}

然后我们用上面两种方式分别拷贝一次

JSON法


JSON.parse(JSON.stringify(obj))

输出结果:
489daa1361cbb57373a3a0abb5b777c05a7bd815

可以从中看出,obj中的普通对象和数组都能拷贝,然而date对象成了字符串,函数直接就不见了,正则成了一个空对象。

再来看看for...in加递归的方法

递归


function isObj(obj) {
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function deepCopy(obj) {
    let tempObj = Array.isArray(obj) ? [] : {}
    for(let key in obj) {
        tempObj[key] = isObj(obj[key]) ? deepCopy(obj[key]) : obj[key]
    }
    return tempObj
}

结果:
c9f095d6c1a4bfefb02707f32e2f0dcb8e3e7d9a
结论

通过上面的测试可知,这两个方法都无法拷贝函数,datereg类型的对象;

什么是环?

环就是对象循环引用,导致自己成为一个闭环,例如下面这个对象:



var a = {}

a.a = a

136b6c8325ddd1268bc44d44a45362faddad134e

使用上面两个方法拷贝一下会直接报错

70d2a7207d2f673c3deac6ee117665d72e70309b

解决方案

可以使用一个WeakMap结构存储已经被拷贝的对象,每一次进行拷贝的时候就先向WeakMap查询该对象是否已经被拷贝,如果已经被拷贝则取出该对象并返回,将 deepCopy函数改造成如下



function deepCopy(obj, hash = new WeakMap()) {
    if(hash.has(obj)) return hash.get(obj)
    let cloneObj = Array.isArray(obj) ? [] : {}
    hash.set(obj, cloneObj)
    for (let key in obj) {
        cloneObj[key] = isObj(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
    }
    return cloneObj
}


拷贝环结果:

1756c2bc20bfdcdc88e501b005dd4aa7638d6193

  • 特殊对象的拷贝

这个问题的解决比较麻烦,因为需要特别对待的对象种类实在太多,于是我参考了MDN上的结构化拷贝,然后结合解决环的方案:



// 只解决date,reg类型,其他的可以自己添加

function deepCopy(obj, hash = new WeakMap()) {
    let cloneObj
    let Constructor = obj.constructor
    switch(Constructor){
        case RegExp:
            cloneObj = new Constructor(obj)
            break
        case Date:
            cloneObj = new Constructor(obj.getTime())
            break
        default:
            if(hash.has(obj)) return hash.get(obj)
            cloneObj = new Constructor()
            hash.set(obj, cloneObj)
    }
    for (let key in obj) {
        cloneObj[key] = isObj(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
    }
    return cloneObj
}

拷贝结果:

89440f0c55d3ecdc19953f213dd053dd8753d9f9

完整版可以查看lodash深拷贝

  • 函数的拷贝

但是MDN上的结构化拷贝依旧没有解决函数的拷贝

f7a5933ff665fac881cc969c938b19fc020df7ea

目前为止,我只想到使用eval的方法对函数进行拷贝,但是这种方法只能对箭头函数生效,如果是fun(){}这种形式的则会出错

拷贝函数增加函数类型

8b44dd25475226f892d1b69893d15f0f4d7cf1b8

出错类型

5458b6356747f226f3f6428a2a08c0b439d9d8f0

后记

JavaScript的深拷贝还不止上面所说的这些坑,还存在的问题有如何拷贝原型链上的属性?如何拷贝不可枚举属性? 如何拷贝Error对象等等的坑,在这里就不一一赘述了。

不过在日常过程中还是建议使用JSON方法,这个方法已经覆盖了绝大部分的业务需求,所以不需要把简单的事情复杂化,不过面试中如果遇到面试官钻牛角尖对这个问题的解答绝对可以秀他一脸了。



原文发布时间为:2018年06月15日
原文作者:YDJFE
本文来源:  掘金   如需转载请联系原作者