「手写题」6、深拷贝

87 阅读5分钟

基础版本

深拷贝的核心就是浅拷贝 + 递归,即通过不断的递归到达对象的最里层,完成基本类型属性的拷贝。

先从最基础的深拷贝版本看起,即只考虑数组和对象字面量的情况:

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) 呢?因为如果传进来的 targetnew 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
}