18888628835/Blog

手写深拷贝

18888628835 opened this issue · 0 comments

手写深拷贝

深拷贝是什么

在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的数据类型进行分辨,其余的简单数据类型都只要直接返回即可。

知道了深拷贝的概念,有了深拷贝的思路

下面我们从0开始,手撸一个深拷贝

简单数据类型深拷贝

简单数据是不可变的

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

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

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

function deepClone(target){
   return target
}

object 类型深拷贝

深拷贝中最重要的是解决 object 类型深拷贝的思路,基于这个复杂数据类型,我们需要对Javascript 中的各种对象(数组对象,函数对象等)进行深拷贝的实现。

function cloneDeep(target) {
  let result
  if (typeof target === 'object') {
    result = {}
    for (let key in target) {
      result[key] = cloneDeep(target[key])
    }
    return result
  }
  return target
}

上面的代码通过对类型的检测,如果发现传入的是一个 object 对象,那么就循环它的属性,并且通过递归的方式一一将 object 对象的属性打到新创建的空白对象中,并将其返回。

Array 类型的深拷贝

在对数组类型进行检测的时候,我们不能用 typeof 进行检测,因为会返回 object,这里我们需要使用的Object.prototype.toString.call(Array)或者instanceof关键字,为了方便,这里就使用 instanceof 关键字

function cloneDeep(target) {
  let result
  if (typeof target === 'object') {
    if (target instanceof Array) {
      result = []
      for (let key of target) {
        result.push(cloneDeep(key))
      }
    } else {
      result = {}
      for (let key in target) {
        result[key] = cloneDeep(target[key])
      }
    }
    return result
  }
  return target
}

函数深拷贝

函数需要怎么深拷贝呢?我们知道函数中的参数传递都是值传递,虽然目标函数会改变引用地址,但是函数已经把值作为参数传递进去了,所以我们直接返回调用函数的结果就可以了。

let fn=function (){return 123}
cloneDeep(fn) / /值传递
fn=function (){return 456} // 这里的函数改变了引用地址
cloneDeep(fn)() //需要做到返回123

下面是实现代码,为了维持代码的结构,让简单数据类型和复杂数据类型做分离,我使用 instanceof 检测 target 类型,当检测函数时才使用 typeof 关键字

function cloneDeep(target) {
  let result
  if (target instanceof Object) {
    if (target instanceof Array) {
      result = []
      for (let key of target) {
        result.push(cloneDeep(key))
      }
    } else if (typeof target === 'function') {
      result = function(...rest) {
        return target(...rest)
      }
    } else {
      result = {}
      for (let key in target) {
        result[key] = cloneDeep(target[key])
      }
    }
    return result
  }
  return target
}

还有更多的数据对象如果想要实现深拷贝,只要新增类型判断即可,这里就不再扩展了。

结束~

enjoy!