深拷贝能够说是前端面试中很是高频的问题,也是一道基础题。所谓的基础不是说深拷贝自己是一个很是简单、很是基础的问题,而是面试官要经过深拷贝来考察候选人的JavaScript基础,甚至是程序设计能力。前端
第一个问题,也是最浅显的问题,为何 JavaScript 中须要深拷贝?或者说若是不使用深拷贝复制对象会带来哪些问题?面试
咱们知道在 JavaScript 中存在“引用类型“和“值类型“的概念。由于“引用类型“的特殊性,致使咱们复制对象不能经过简单的clone = target
,因此须要把原对象的属性值一一赋给新对象。数组
而对象的属性其值也多是另外一个对象,因此咱们须要递归。spa
经过for...in
可以遍历对象上的属性;也能够经过Object.keys(target)
获取到对象上的属性数组后再进行遍历。
这里选用for...in
由于相比Object.keys(target)
它还会遍历对象原型链上的属性。prototype
ES6 Symbol 类型也能够做为对象的 key ,如何获取它们?设计
可使用typeof
判断目标是否为引用类型,这里有一处须要注意:typeof null
也是object
:code
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或者正则这样的对象。为何?blog
获取一个对象具体类型有哪些方式?教程
经常使用的方式有target.constructor.name
、Object.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好在哪儿?
以上咱们就完成了一个基础的深拷贝。可是它仅仅是及格而已,想要作到优秀,还要处理一下以前留下的几个问题。
ES6Symbol
类型也能够做为对象的 key ,可是for...in
和Object.keys(target)
都拿不到 Symbol
类型的属性名。
好在咱们能够经过Object.getOwnPropertySymbols(target)
获取对象上全部的Symbol
属性,再结合for...in
、Object.keys()
就可以拿到所有的 key。不过这种方式有些麻烦,有没有更好用的方法?
有!Reflect.ownKeys(target)
正是这样一个集优雅与强大与一身的方法。可是正如同人无完人,这个方法也不完美:顾名思义,ownKeys
是拿不到原型链上的属性的。因此须要结合具体场景来组合使用上述方法。
Date
、Error
等特殊的内置类型虽然是对象,可是并不能遍历属性,因此针对这些类型须要从新调用对应的构造器进行初始化。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
持有的是每一个键对象的“弱引用”,这意味着在没有其余引用存在时垃圾回收能正确进行。若是 target 很是庞大,那么使用Map
后若是没有进行手动释放,这块内存就会持续的被占用。而WeakMap
则不须要担忧这个问题。
若是上面几个问题都获得了妥善的处理,那么这样的深拷贝就能够说是一个足够打动面试官的深拷贝了。固然这个深拷贝还不够优秀,有不少待完善的地方,相信善于思考的你已经有了本身的思路。
但本文的重点并不仅仅是实现一个深拷贝,更多的是但愿它可以帮助你更好的理解面试官的思路,从而更好的发挥自身的能力。
关注「JS漫步指南」公众号,获取更多面试秘籍!