写代码第一层境界:完成功能
第二层境界:考虑边界
第三层境界:优化
-- 改编自某面试官的面试标准
手写深拷贝,你可能会想着用递归,写出下面的代码。
function cloneDeep(source) {
if (typeof source !== "object" || source == null) {
return source;
}
let res = source instanceof Array ? [] : {};
for (let key in source) {
if (source.hasOwnProperty(key)) {
res[key] = cloneDeep(source[key]);
}
}
return res;
}
整个代码的思路是:如果是基础数据类型,直接返回,如果不是,说明是数组或者对象,进行遍历。hasOwnProperty 是为了过滤原型链的属性。因为 for in 会循环原型链的属性,最后递归。
但是你会发现存在很多问题,比如循环引用,map({})、set({})、weakMap({})、weakSet({})、日期(变成字符串)、正则对象({}),这些数据类型变空对象或者字符串,symbol 作为属性会变成 string。
我们首先解决循环引用导致栈溢出的问题。
思路是用缓存,把数据存下来,如果已经存过了,就直接返回。没存过就存起来。这样就不会顺着引用的对象一直找下去。
这里不用 object 也不用 map,用 weakMap,对象有个问题,键不能是个值,只能是 string。weakMap 是弱引用,不会内存泄漏。
改造一下。
function cloneDeep(source, map = new WeakMap()) {
if (map.get(source)) {
return map.get(source);
}
//基础数据类型
if (typeof source !== "object" || source == null) {
return source;
}
//和要拷贝的对象保持类一致,例如对象或数组
const res = new source.constructor();
map.set(source, res);
for (let key in source) {
if (source.hasOwnProperty(key)) {
res[key] = cloneDeep(source[key], map);
}
}
return res;
}
接下来把数据类型补上。
function cloneDeep(source, map = new WeakMap()) {
if (map.get(source)) {
return map.get(source);
}
//基础数据类型
if (typeof source !== "object" || source == null) {
return source;
}
//正则
if (source instanceof RegExp) return new RegExp(source);
//日期
if (source instanceof Date) return new Date(source);
//set
if (source instanceof Set) {
const newSet = new Set();
source.forEach((item) => {
newSet.add(cloneDeep(item));
});
return newSet;
}
//map
if (source instanceof Map) {
const newMap = new Map();
source.forEach((value, key) => {
newMap.set(key, cloneDeep(value));
});
return newMap;
}
const res = new source.constructor();
//和要拷贝的对象保持类一致,例如对象或数组
map.set(source, res);
for (let key in source) {
if (source.hasOwnProperty(key)) {
res[key] = cloneDeep(source[key], map);
}
}
return res;
}
但是 symbol 作为属性识别不了,所以用 Object.getOwnPropertySymbols 识别 symbol 作为属性并拷贝
function cloneDeep(source, map = new WeakMap()) {
...
//处理symbol作为对象属性的情况
const symbolKeys = Object.getOwnPropertySymbols(source);
for (const symKey of symbolKeys) {
res[Symbol(symKey.description)] = cloneDeep(source[symKey]);
}
return res;
}
最后优化一下代码,把 Date, Map, Set, RegExp 一起处理,传给构造函数去生成。
function cloneDeep(source, map = new WeakMap()) {
if (map.get(source)) {
return map.get(source);
}
//基础数据类型
if (typeof source !== "object" || source == null) {
return source;
}
//特色对象
const types = [Date, Map, Set, RegExp];
if (types.includes(source.constructor)) return new source.constructor(source);
const res = new source.constructor();
//和要拷贝的对象保持类一致,例如对象或数组
map.set(source, res);
for (let key in source) {
if (source.hasOwnProperty(key)) {
res[key] = cloneDeep(source[key], map);
}
}
//处理symbol作为对象属性的情况
const symbolKeys = Object.getOwnPropertySymbols(source);
for (const symKey of symbolKeys) {
res[Symbol(symKey.description)] = cloneDeep(source[symKey]);
}
return res;
}
但是这个代码仍然有问题, WeakMap 和 WeakSet 依然解决不了,因为他们不可枚举。