Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

你以为面试官在问深拷贝的时候,仅仅是在问深拷贝吗? #6

Open
hengg opened this issue Aug 1, 2020 · 0 comments
Labels
blog blog

Comments

@hengg
Copy link
Owner

hengg commented Aug 1, 2020

深拷贝可以说是前端面试中非常高频的问题,也是一道基础题。所谓的基础不是说深拷贝本身是一个非常简单、非常基础的问题,而是面试官要通过深拷贝来考察候选人的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则不需要担心这个问题。

后记

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

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

参考资料

关注「JS漫步指南」公众号,获取更多面试秘籍

@hengg hengg added the blog blog label Aug 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blog blog
Projects
None yet
Development

No branches or pull requests

1 participant