33

deep.001.jpeg

深拷贝可以说是前端面试中非常高频的问题,也是一道基础题。所谓的基础不是说深拷贝本身是一个非常简单、非常基础的问题,而是面试官要通过深拷贝来考察候选人的JavaScript基础,甚至是程序设计能力。

为什么需要深拷贝?

第一个问题,也是最浅显的问题,为什么 JavaScript 中需要深拷贝?或者说如果不使用深拷贝复制对象会带来哪些问题?

我们知道在 JavaScript 中存在“引用类型“和“值类型“的概念。因为“引用类型“的特殊性,导致我们复制对象不能通过简单的clone = target,所以需要把原对象的属性值一一赋给新对象。

而对象的属性其值也可能是另一个对象,所以我们需要递归

如何获取原对象的属性?

通过for...in能够遍历对象上的属性;也可以通过Object.keys(target)获取到对象上的属性数组后再进行遍历。
这里选用for...in因为相比Object.keys(target)它还会遍历对象原型链上的属性。

ES6 Symbol 类型也可以作为对象的 key ,如何获取它们?

如何判断对象的类型?

可以使用typeof判断目标是否为引用类型,这里有一处需要注意:typeof null也是object

function deepClone(target) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{}
        for (const key in target) {
            clone[key] = deepClone(target[key])
        }
        return clone;
    }
    return target;
}

上述代码就完成了一个非常基础的深拷贝。但是对于引用类型的处理,它仍然是不完善的:

它没法处理Date或者正则这样的对象。为什么?

“回字的四样写法“--具体类型的识别

获取一个对象具体类型有哪些方式?

常用的方式有target.constructor.nameObject.prototype.toString.call(target)instanceOf

  • instacneOf可以用来判断对象类型,但是Date的实例同时也是Object的实例,此处用于判断是不准确的;
  • target.constructor.name得到的是构造器名称,而构造器是可以被修改的;
  • Object.prototype.toString.call(target)返回的是类名,而在ES5中只有内置类型对象才有类名。

所以此处我们最合适的选择是Object.prototype.toString.call(target)

Object.prototype.toString.call(target)也存在一些问题,你知道吗?

稍微改进一下代码,做一些简单的类型判断:

function deepClone(target) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{};

        if(Object.prototype.toString.call(target) === '[object Date]'){
            clone = new Date(target)
        }
        
        if(Object.prototype.toString.call(target) === '[object Object]'
        ||Object.prototype.toString.call(target) === '[object Array]'){
            for (const key in target) {
                clone[key] = deepClone(target[key])
            }
        }

        return clone;
    }
    return target;
}

怎么能够更优雅的做类型判断?

你听说过“循环引用“吗?

假如目标对象的属性间接或直接的引用了自身,就会形成循环引用,导致在递归的时候爆栈。
所以我们的代码需要循环检测,设置一个Map用于存储已拷贝过的对象,当检测到对象已存在于Map中时,取出该值并返回即可避免爆栈。

function deepClone(target, map = new Map()) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{};
        if (map.get(target)) {
            return map.get(target);
        }
        
        map.set(target, clone);

        if(Object.prototype.toString.call(target) === '[object Date]'){
            clone = new Date(target)
        }
        
        if(Object.prototype.toString.call(target) === '[object Object]'
            ||Object.prototype.toString.call(target) === '[object Array]'){
            for (const key in target) {
                clone[key] = deepClone(target[key],map)
            }
        }

        return clone;
    }
    return target;
}

好多教程使用 WeakMap 做存储,相比Map,WeakMap好在哪儿?

通往优秀的阶梯

以上我们就完成了一个基础的深拷贝。但是它仅仅是及格而已,想要做到优秀,还要处理一下之前留下的几个问题。

获取Symbol属性

ES6Symbol类型也可以作为对象的 key ,但是for...inObject.keys(target)都拿不到 Symbol类型的属性名。

好在我们可以通过Object.getOwnPropertySymbols(target) 获取对象上所有的Symbol属性,再结合for...inObject.keys()就能够拿到全部的 key。不过这种方式有些麻烦,有没有更好用的方法?

有!Reflect.ownKeys(target) 正是这样一个集优雅与强大与一身的方法。但是正如同人无完人,这个方法也不完美:顾名思义,ownKeys是拿不到原型链上的属性的。所以需要结合具体场景来组合使用上述方法。

特殊的内置类型

DateError等特殊的内置类型虽然是对象,但是并不能遍历属性,所以针对这些类型需要重新调用对应的构造器进行初始化。JavaScript 内置了许多类似的特殊类型,然而我们并不是无情的 API 机器,面试中能够回答上述要点也就足够了。

上述内置类型我们都可以通过Object.prototype.toString.call(target) 的方式拿到,所以这里可以封装一个类型判断的方法用于判断target 是否能够继续遍历,以便于及后续的处理。

然而 ES6 新增了Symbol.toStringTag方法,可以用来自定义类名,这就导致 Object.prototype.toString.call(target)拿到的类型名也可能不够准确:

class ValidatorClass {
  get [Symbol.toStringTag]() {
    return "Validator";
  }
}

Object.prototype.toString.call(new ValidatorClass()); 
// "[object Validator]"

使用WeakMap做循环检测,比使用Map好在哪儿?

原生的WeakMap持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。如果 target 非常庞大,那么使用Map 后如果没有进行手动释放,这块内存就会持续的被占用。而WeakMap则不需要担心这个问题。

后记

如果上面几个问题都得到了妥善的处理,那么这样的深拷贝就可以说是一个足够打动面试官的深拷贝了。当然这个深拷贝还不够优秀,有很多待完善的地方,相信善于思考的你已经有了自己的思路。

但本文的重点并不单单是实现一个深拷贝,更多的是希望它能够帮助你更好的理解面试官的思路,从而更好的发挥自身的能力。

参考资料

qr.001.jpeg


王亮hengg
456 声望1.1k 粉丝

资深拷贝工程师