自己动手实现一个深拷贝

818 阅读6分钟

深拷贝是什么

在JS中,所有的拷贝API都是浅拷贝,比如数组的拷贝,我们一般使用Array.prototype.slice来拷贝一个数组,但是对于嵌套数组,就会只拷贝其中的引用。

const arr=[[1,2,3],[4,5,6]]
const arr2=arr.slice()
arr2[0].push(666)
console.log(arr) //[[1, 2, 3, 666], [4, 5, 6]]

上面的arr2是拷贝后的数组,arr2改变了同样会影响到arr的值,所以这就不是深拷贝。

官方解释是这样的

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

我所理解的深拷贝,就是当我克隆一个东西出来之后,跟原来的完全不相交。

序列化与反序列化

在工作中,我们一般都会使用序列化进行深拷贝,就是采用JSON.stringifyJSON.parse来进行深拷贝

const obj={
   name:'yanxi',
   props:{name:'qiu'}
}
const obj2=JSON.parse(JSON.stringify(obj))
obj2.props.name='11111'
obj 
//{name: "yanxi", props: {name: "qiu"}}

这种方式非常简单,但JSON的局限性较大。

局限性如下:

1、不支持函数,会自动忽略 2、不支持undefined,JSON天然不支持 3、不支持环状引用(即引用自身)会报错

4、不支持Date,会转成字符串 5、不支持symbol,JSON天然不支持

我们可以看到JSON.parse(JSON.stringify())虽然能够深拷贝一个对象,但是存在很大的局限性,对于复杂的对象就不适用了。因此,我们需要采用另外的方式来实现深拷贝,也就是通过递归的方式手动实现深拷贝。

递归深拷贝

JS中存在七种类型:number、string、boolean、symbol、null、undefined、object

其中除了object属于引用类型,其余都是简单数据类型,所以我们主要要对object的数据类型进行分辨,其余的简单数据类型都只要直接返回即可。

简单数据类型深拷贝

简单数据是不可变的

let a=1
let b=a
b=2
a //1

上面的代码只是改变了b的指向,并不影响原来的数据a。

那么我们就可以直接实现对于简单数据类型的深拷贝函数

function deepClone(target){
   return target
}

普通对象深拷贝

const obj={
   name:`qiuyanxi`,
   fullMessage:{
      firstName:'qiu',
      age:20,
   }
}

上面就是一个普通对象,我们通过检测其类型,然后创建一个新对象,对源数据循环后递归来返回源数据的具体属性

只要对象有尽头,最终的具体数据肯定是简单数据类型,那么只要最终找到底层数据,然后拷贝出去就行了。这也是递归深拷贝的核心思想。

function deepClone(target){
   if(target instanceof Object){//普通对象深拷贝
      const newObj=Object.create(null)
      for(let k in target){
         newObj[k]=deepClone(target[k])
      }
     return newObj
   }
   return target
}

上面的代码创建了一个新的对象,然后通过递归把每个对象上的属性拷贝到新对象上。

如果这个属性是简单类型的那么就直接返回这个属性值。如果是Object类型,那么就通过for...in遍历讲对象上的每个属性一个一个地添加到新的对象身上。因为无法区分对象的层级,因此使用递归,每次赋值时都是调用自己,反正如果是简单类型就递归一次直接返回值,如果是Object类型,那么就往下递归查找赋值。

一般的递归是要设置终止条件的,但是对象因为自身存在终止条件,即到达其原型链最顶层会自动停止,所以我们不需要再设置终止条件。

数组的深拷贝

数组可以使用for of循环

function deepClone(target){
   if(target instanceof Object){//普通对象深拷贝
     if(target instanceof Array){//数组深拷贝
       const newArr=[]
       for(let v of target){
         newArr.push(deepClone(v))
       }
       return newArr
     }
      const newObj=Object.create(null)
      for(let k in target){
         newObj[k]=deepClone(target[k])
      }
     return newObj
   }
   return target
}

函数深拷贝

因为在很多人看来函数是无法拷贝的。在我看来函数实际上不应该有深拷贝的,如果真的要有,那么也就是实现函数的功能。

应该如何实现一个函数的拷贝了?

  • 需要返回一个新的函数
  • 新的函数执行结果必须与原函数相同。
function deepClone(target){
   if(target instanceof Object){//普通对象深拷贝
     if(target instanceof Array){//数组深拷贝
       const newArr=[]
       for(let v of target){
         newArr.push(deepClone(v))
       }
       return newArr
     }else if(target instanceof Function){//函数深拷贝
        return (...args)=>{
           return target.call(this,...args)
        }
     }
      const newObj=Object.create(null)
      for(let k in target){
         newObj[k]=deepClone(target[k])
      }
     return newObj
   }
   return target
}

正则表达式

正则表达式的形式是这样的/[0-9]{1,2}/ig,我们可以通过两个正则的属性获取到

/[0-9]{1,2}/ig.source //"[0-9]{1,2}"
/[0-9]{1,2}/ig.flags //gi

我们深拷贝一个正则实际上就是拿到这两部分,然后重新创建一个新的正则,从而实现跟原来的正则相同的功能即可.

function deepClone(target){
   if(target instanceof Object){
     if(target instanceof Array){//数组深拷贝
       const newArr=[]
       for(let v of target){
         newArr.push(deepClone(v))
       }
       return newArr
     }else if(target instanceof Function){//函数深拷贝
        return (...args)=>{
           return target.call(this,...args)
        }
     }else if(target instanceof RegExp){ //深拷贝正则
           return new RegExp(target.source,target.flags)
        }
      const newObj=Object.create(null)//普通对象深拷贝
      for(let k in target){
         newObj[k]=deepClone(target[k])
      }
     return newObj
   }
   return target
}

日期深拷贝

日期的拷贝跟正则表达式一样,假设我们需要拷贝一个日期,可以使用new Date构造函数传入原来的日期,就可以完成深拷贝日期操作

function deepClone(target){
   if(target instanceof Object){
     if(target instanceof Array){//数组深拷贝
       const newArr=[]
       for(let v of target){
         newArr.push(deepClone(v))
       }
       return newArr
     }else if(target instanceof Function){//函数深拷贝
        return (...args)=>{
           return target.call(this,...args)
        }
     }else if(target instanceof RegExp){ //深拷贝正则
           return new RegExp(target.source,target.flags)
        }else if(target instanceof Date){
           return new Date(target) //深拷贝日期
        }
      const newObj=Object.create(null)//普通对象深拷贝
      for(let k in target){
         newObj[k]=deepClone(target[k])
      }
     return newObj
   }
   return target
}

优化后的代码

function deepClone(target) {
  if (target instanceof Object) {
    let result = {};  //普通对象深拷贝
    if (target instanceof Array) {
      result = []; //数组深拷贝
    } else if (target instanceof Function) {
      //函数深拷贝
      result = (...args) => {
        return target.call(this, ...args);
      };
    } else if (target instanceof RegExp) {
      //深拷贝正则
      result = new RegExp(target.source, target.flags);
    } else if (target instanceof Date) {
      result = new Date(target); //深拷贝日期
    }
    Object.keys(target).forEach((k)=>{
       result[k]=deepClone(target(k))
    })//使用这个代替for in
    return result;
  }
  return target;
}

由于for in会循环出原型链上的属性,所以在这里我改了成 Object.keys(target)forEach循环来遍历对象。