基础版本
深拷贝的核心就是浅拷贝 + 递归,即通过不断的递归到达对象的最里层,完成基本类型属性的拷贝。
先从最基础的深拷贝版本看起,即只考虑数组和对象字面量的情况:
function deepClone(target){
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? []:{}
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key])
})
return cloneTarget
} else {
return target
}
}
根据初始传入的 target 是一个对象字面量还是数组,决定最终返回的 cloneTarget 是对象还是数组。接着遍历 target 的每一个自身可枚举属性,递归调用 deepClone,如果属性已经是基本类型,则直接返回;如果还是对象或者数组,就和初始的 target 进行一样的处理。最后,把处理好的结果一一拷贝给 cloneTarget。
解决循环引用
对于初次传入的对象或者数组,使用一个 WeakMap 记录当前目标和拷贝结果的映射关系,当检测到再次传入相同的目标时,不再进行重复的拷贝,而是直接从 WeakMap 中取出它对应的拷贝结果返回。
改进后的代码如下:
function deepClone(target,map = new WeakMap()){
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? []:{}
// 处理循环引用的问题
if(map.has(target)) return map.get(target)
map.set(target,cloneTarget)
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key],map)
})
return cloneTarget
} else {
return target
}
}
处理其它数据类型
深拷贝对象,这个对象应该理解为引用类型,所以它其实还包括了很多种类:除了上面已经处理的对象字面量和数组,还有 Set、Map、类数组对象、函数、基本类型的包装类型等。
1)类型判断函数
为了更好地判断是引用数据类型还是基本数据类型,可以使用一个 isObject 函数:
function isObject(o){
return o !== null && (typeof o === 'object' || typeof o === 'function')
}
这样,所有的对象字面量、数组、Set、Map、类数组对象、函数、基本类型的包装类型等,都视为 object。
为了更准确地判断具体是什么数据类型,可以使用一个 getType 函数:
function getType(o){
return Object.prototype.toString.call(o).slice(8,-1)
}
// getType(1) "Number"
// getType(null) "Null"
2)初始化函数
之前深拷贝对象字面量或者数组的时候,首先会将最终返回的结果 cloneTarget 初始化为 [] 或者 {}。同样地,对于 Set、Map 以及类数组对象,也需要进行相同的操作,所以最好用一个函数统一实现 cloneTarget 的初始化。
function initCloneTarget(target){
return new target.constructor()
}
通过 target.constructor 可以获得传进来的实例的构造函数,利用这个构造函数新创建一个同类型的实例并返回。
3)处理可以继续遍历的对象:Set、Map、类数组对象
处理 Set 和 Map 的流程基本和对象字面量以及数组差不多,但是不能采用直接赋值的方式,而要使用 add 方法或者 set 方法,所以稍微改进一下。至于类数组对象,其实和数组以及对象字面量的形式差不多,所以可以一块处理。
代码如下:
function deepClone(target,map = new WeakMap()){
// 如果是基本类型,直接返回即可
if(!isObject(target)) return target // 初始化返回结果
let type = getType(target)
let cloneTarget = initCloneTarget(target)
// 处理循环引用
if(map.has(target)){
return map.get(target)
} else {
map.set(target,cloneTarget)
}
// 处理 Set
if(type === 'Set'){
target.forEach(value => {
cloneTarget.add(deepClone(value,map))
})
}else if(type === 'Map'){
// 处理 Map
target.forEach((value,key) => {
cloneTarget.set(key,deepClone(value,map))
})
}else if(type === 'Object' || type === 'Array' || type === 'Arguments'){
// 处理对象字面量、数组、类数组对象
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key],map)
})
}
return cloneTarget
}
4)处理不能继续遍历的对象:函数、错误对象、日期对象、正则对象、基本类型的包装对象
对于上面这些对象,我们不能像基本数据类型那样直接返回,否则将返回相同的引用,并没有达到拷贝的目的。正确的做法,应该是拷贝一份副本,再直接返回。
如何拷贝呢?这里又分为两种情况。其中,String、Boolean、Number、错误对象、日期对象都可以通过 new 的方式返回一个实例副本;而 Symbol、函数、正则对象的拷贝则无法通过简单的 new 拷贝副本,需要单独处理。
拷贝 Symbol
function cloneSymbol(target){
return Object(target.valueOf()) // 或者
return Object(Symbol.prototype.valueOf.call(target))
// 或者
return Object(Symbol(target.description))
}
PS:这里的 target 是 Symbol 基本类型的包装类型,调用 valueOf 可以获得它对应的拆箱结果,再把这个拆箱结果传给 Object,就可以构造原包装类型的副本了;为了保险起见,可以通过 Symbol 的原型调用 valueOf;可以通过 description 获得 symbol 的描述符,基于此也可以构造原包装类型的副本。
拷贝正则对象(参考 lodash 的做法)
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new RegExp(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
拷贝函数(实际上函数没有必要拷贝)
function cloneFunction(target){
return eval(`(${target})`)
// 或者
return new Function(`return (${target})()`)
}
PS:传给 new Function 的参数声明了新创建的函数实例的函数体内容
接下来,用一个 directCloneTarget 函数处理以上所有情况:
function directCloneTarget(target,type){
let _constructor = target.constructor
switch(type){
case 'String':
case 'Boolean':
case 'Number':
case 'Error':
case 'Date':
return new _constructor(target.valueOf())
// 或者
//return new Object(target.valueOf())
// 或者
//return new Object(_constructor.prototype.valueOf.call(target))
case 'RegExp':
return cloneReg(target)
case 'Symbol':
return cloneSymbol(target)
case 'Function':
return cloneFunction(target)
default:
return null
}
}
PS:注意这里有一些坑。
- 为什么使用
return new _constructor(target.valueOf())而不是return new _constructor(target)呢?因为如果传进来的target是new Boolean(false),那么最终返回的实际上是new Boolean(new Boolean(false)),由于参数并非空对象,因此它的值对应的不是期望的 false,而是 true。所以,最好使用valueOf获得包装类型对应的真实值。 - 也可以不使用基本类型对应的构造函数
_constructor,而是直接new Object(target.valueOf())对基本类型进行包装 - 考虑到 valueOf 可能被重写,为了保险起见,可以通过基本类型对应的构造函数
_constructor去调用 valueOf 方法
最终版本
// 可以继续遍历的类型
const objectToInit = ['Object','Array','Set','Map','Arguments']
// 判断是否是引用类型
function isObject(o){
return o !== null && (typeof o === 'object' || typeof o === 'function')
}
// 判断具体的数据类型
function getType(o){
return Object.prototype.toString.call(o).slice(8,-1)
}
// 初始化函数
function initCloneTarget(target){
return new target.constructor()
}
// 拷贝 Symbol
function cloneSymbol(target){
return Object(target.valueOf()) // 或者
return Object(Symbol.prototype.valueOf.call(target))
// 或者
return Object(Symbol(target.description))
}
// 拷贝正则对象
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new RegExp(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
// 拷贝函数
function cloneFunction(target){
return new Function(`return (${target})()`)
// 或者 return eval(`(${target})`)
}
// 处理不能继续遍历的类型
function directCloneTarget(target,type){
let _constructor = target.constructor
switch(type){
case 'String':
case 'Boolean':
case 'Number':
case 'Error':
case 'Date':
return new _constructor(target.valueOf())
// 或者
//return new Object(_constructor.prototype.valueOf.call(target))
case 'RegExp':
return cloneReg(target)
case 'Symbol':
return cloneSymbol(target)
case 'Function':
return cloneFunction(target)
default:
return null
}
}
// 深拷贝的核心代码
function deepClone(target,map = new WeakMap()){
if(!isObject(target)) return target // 初始化
let type = getType(target)
let cloneTarget
if(objectToInit.includes(type)){
cloneTarget = initCloneTarget(target)
} else {
return directCloneTarget(target,type)
}
// 解决循环引用
if(map.has(target)){
return map.get(target)
}else {
map.set(target,cloneTarget)
}
// 拷贝 Set
if(type === 'Set'){
target.forEach(value => {
cloneTarget.add(deepClone(value,map))
})
}else if(type === 'Map'){
// 拷贝 Map
target.forEach((value,key) => {
cloneTarget.set(key,deepClone(value,map))
})
}else if(type === 'Object' || type === 'Array' || type === 'Arguments'){
// 拷贝对象字面量、数组、类数组对象
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key],map)
})
}
return cloneTarget
}