深拷贝能够说是前端面试中十分高频的问题,也是一道根底题。所谓的根底不是说深拷贝自身是一个非常简单、十分根底的问题,而是面试官要通过深拷贝来考查候选人的 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.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 好在哪儿?
通往优良的阶梯
以上咱们就实现了一个根底的深拷贝。然而它仅仅是及格而已,想要做到优良,还要解决一下之前留下的几个问题。
获取 Symbol 属性
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 做循环检测,比应用 Map 好在哪儿?
原生的 WeakMap
持有的是每个键对象的“弱援用”,这意味着在没有其余援用存在时垃圾回收能正确进行。如果 target 十分宏大,那么应用 Map
后如果没有进行手动开释,这块内存就会继续的被占用。而WeakMap
则不须要放心这个问题。
后记
如果下面几个问题都失去了妥善的解决,那么这样的深拷贝就能够说是一个足够感动面试官的深拷贝了。当然这个深拷贝还不够优良,有很多待欠缺的中央,置信长于思考的你曾经有了本人的思路。
但本文的重点并不单单是实现一个深拷贝,更多的是心愿它可能帮忙你更好的了解面试官的思路,从而更好的施展本身的能力。
参考资料
- lodash
- Global_Objects