手写深拷贝

深拷贝是什么

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

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

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

官方解释是这样的

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

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

序列化与反序列化的深拷贝

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

js
1const obj = {
2  name: 'yanxi',
3  props: { name: 'qiu' },
4}
5const obj2 = JSON.parse(JSON.stringify(obj))
6obj2.props.name = '11111'
7obj
8//{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 开始,手撸一个深拷贝

简单数据类型深拷贝

简单数据是不可变的

js
1let a = 1
2let b = a
3b = 2
4a //1

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

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

js
1function deepClone(target) {
2  return target
3}

object 类型深拷贝

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

javascript
1function cloneDeep(target) {
2  let result
3  if (typeof target === 'object') {
4    result = {}
5    for (let key in target) {
6      result[key] = cloneDeep(target[key])
7    }
8    return result
9  }
10  return target
11}

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

Array 类型的深拷贝

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

javascript
1function cloneDeep(target) {
2  let result
3  if (typeof target === 'object') {
4    if (target instanceof Array) {
5      result = []
6      for (let key of target) {
7        result.push(cloneDeep(key))
8      }
9    } else {
10      result = {}
11      for (let key in target) {
12        result[key] = cloneDeep(target[key])
13      }
14    }
15    return result
16  }
17  return target
18}

函数深拷贝

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

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

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

javascript
1function cloneDeep(target) {
2  let result
3  if (target instanceof Object) {
4    if (target instanceof Array) {
5      result = []
6      for (let key of target) {
7        result.push(cloneDeep(key))
8      }
9    } else if (typeof target === 'function') {
10      result = function(...rest) {
11        return target(...rest)
12      }
13    } else {
14      result = {}
15      for (let key in target) {
16        result[key] = cloneDeep(target[key])
17      }
18    }
19    return result
20  }
21  return target
22}

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

结束~

enjoy!