分析
深拷贝函数也是一个老生常谈的话题了,它的实现有很多函数库的版本,例如 lodash 的 _.cloneDeep。
或者图个省事就直接 JSON.parse(JSON.stringify())
,当然这么做有许多缺点,没有考虑循环引用问题,也没有考虑其他一些数据类型的不便如 BigInt,Map,Set,Date,其中 BigInt 还是基础类型。
那么综上所述,我们该做的深拷函数不必像 lodash 那么复杂,一段函数清晰明了,也可以兼容处理 JSON.parse 的那些缺点。那就给出下面的结构开始实现吧
function deepCloneDFS(origin) {
}
var res = deepCloneDFS({
num: 1,
string: 'abc',
arr: [1, 2],
obj: {
nul: null,
undef: undefined
}
});
console.log(res);
策略模式版本
促使笔者复习深拷,并重写它,就是因为最近学习到了策略模式,以及复习到了 DFS 深搜。
递归 origin,并拿到它的类型进行策略判定,满足某一个策略,就用这个策略来执行并返回,若没有命中策略,返回自身。代码就非常简单:
function deepCloneDFS(origin) {
// 命中策略
const constructor = Object.prototype.toString.call(origin);
const fn = strategy[constructor];
if (fn) {
return fn(origin);
}
// 没有命中策略的,使用自身的构造函数重建一个
const constructor = origin.constructor;
return new constructor(origin);
}
// 策略
var strategy = {
'[object Number]': function (origin) { return origin },
'[object String]': function (origin) { return origin },
'[object Boolean]': function (origin) { return origin },
'[object Null]': function (origin) { return origin },
'[object Undefined]': function (origin) { return origin },
'[object Array]': function(origin) {
let result = [];
origin.forEach((item, index) => {
result[index] = deepCloneDFS(item);
})
return result;
},
'[object Object]': function(origin) {
let result = {};
Object.keys(origin).forEach(key => {
result[key] = deepCloneDFS(origin[key]);
})
return result;
}
}
基础版本就这样简单,我们在此还没有判定循环引用问题,和更多类型的问题。
解决循环引用
什么是循环引用?举个例子,执行下面的代码就会栈溢出,因为它无限递归:
var obj = {}
obj.test = obj;
var res = deepCloneDFS(obj);
console.log(res);
// RangeError: Maximum call stack size exceeded
那么解决的核心思想就是遍历的每一级,需要检测对象和数组是否之前已经出现过,那我们就需要准备一个缓存数据。Map 结构可以以对象和数组做键名去存放数据,这就非常适合这个场景。
代码如下,下文注释的地方有修改:
function deepCloneDFS(origin, map = new Map()) {
// 循环引用检测
if (map.get(origin)) {
return origin;
}
// 把对象作为键名
map.set(origin, true)
const constructor = Object.prototype.toString.call(origin);
const fn = strategy[constructor];
if (fn) {
// 传入 map
return fn(origin, map);
}
const constructor = origin.constructor;
return new constructor(origin);
}
var strategy = {
// ... 省略其他基础类型代码
'[object Array]': function(origin, map) {
let result = [];
origin.forEach((item, index) => {
// 传入 map
result[index] = deepCloneDFS(item, map);
})
return result;
},
'[object Object]': function(origin, map) {
let result = {};
Object.keys(origin).forEach(key => {
// 传入 map
result[key] = deepCloneDFS(origin[key], map);
})
return result;
}
}
运行结果
var obj = {
}
obj.test = [
1,2 ,3, obj
];
var res = deepCloneDFS(obj);
console.log(res);
// Object {test: Array(4)}
策略扩充
我们知道 ES6 引入 Symbol,ES 10 引入 BitInt,这都是新的数据类型,需要小心 Symbol('a') === Symbol('b') // false
var strategy = {
// ... 省略其他
'[object Symbol]': function (origin) {
return new Object(Symbol.prototype.valueOf.call(origin));
},
'[object BigInt]': function (origin) { return origin },
}
此外还有 Function、Map、Set 需要特殊处理一下
var strategy = {
// ... 省略其他
'[object Function]': function (origin) {
// 箭头函数直接返回
if (!origin.prototype) {
return new Function(origin.toString());
}
// 普通函数需要正则处理处理
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
},
// Map 得小心它的 key 和 value 都可能是个对象需要深拷贝
'[object Map]': function (origin, map) {
let result = new Map();
origin.forEach((item, key) => {
result.set(deepCloneDFS(key, map), deepCloneDFS(item, map));
})
return result;
},
'[object Set]': function (origin, map) {
let result = new Set();
origin.forEach((item) => {
result.add(deepCloneDFS(item, map));
});
return result;
},
}
运行结果如下
var res = {};
res.func = function (a, b, c) { return a + b + c };
res.map = new Map();
res.map.set('test', 1);
res.map.set(res.func, 1);
res.set = new Set()
res.set.add(res.func);
var clone = deepCloneDFS(res);
console.log(clone); // Object {func: , map: Map(2), set: Set(1)}
console.log(clone.map === res.map) // false
console.log(clone.set === res.set) // false
完整代码
function deepCloneDFS(origin, map = new Map()) {
// 循环引用检测
if (map.get(origin)) {
return origin;
}
// 把对象作为键名
map.set(origin, true)
// 命中策略
let constructorType = Object.prototype.toString.call(origin);
let fn = strategy[constructorType];
if (fn) {
return fn(origin, map);
}
// 没有命中策略的,使用自身的构造函数重建一个
const constructor = origin.constructor;
return new constructor(origin);
}
// 策略
var strategy = {
'[object Number]': function (origin) { return origin },
'[object String]': function (origin) { return origin },
'[object Boolean]': function (origin) { return origin },
'[object Null]': function (origin) { return origin },
'[object Undefined]': function (origin) { return origin },
'[object Symbol]': function (origin) {
return new Object(Symbol.prototype.valueOf.call(origin));
},
'[object BigInt]': function (origin) { return origin },
'[object Function]': function (origin) {
// 箭头函数直接返回
if (!origin.prototype) {
return new Function(origin.toString());
}
// 普通函数需要正则处理处理
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = origin.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
},
// Map 得小心它的 key 和 value 都可能是个对象需要深拷贝
'[object Map]': function (origin, map) {
let result = new Map();
origin.forEach((item, key) => {
result.set(deepCloneDFS(key, map), deepCloneDFS(item, map));
})
return result;
},
'[object Set]': function (origin, map) {
let result = new Set();
origin.forEach((item) => {
result.add(deepCloneDFS(item, map));
});
return result;
},
'[object Array]': function(origin, map) {
let result = [];
origin.forEach((item, index) => {
result[index] = deepCloneDFS(item, map);
});
return result;
},
'[object Object]': function(origin, map) {
let result = {};
Object.keys(origin).forEach(key => {
result[key] = deepCloneDFS(origin[key], map);
});
return result;
}
}
总结
一段函数,兼容几乎全部类型,并且解决循环引用问题,并且精简了代码结构,采用了让代码更容易看懂的设计模式结构,就这样我们都达到了。
要说有什么奇怪的地方,就是函数那里,我们真的有必要拷贝制造出两段功能一样的函数么,看你的工作需要吧。
参考
lodash-cloneDeepWith
Everlose-JS 设计模式-工作常用的
如何写出一个惊艳面试官的深拷贝
github ConardLi-deepClone