Please enable Javascript to view the contents

Js实现深拷贝

 ·  ☕ 5 分钟

深拷贝使用场景:

  1. 默认选项
  2. 部分修改

API: 接收一个对象或者基本类型,对象的类型包括 Array、function、普通 Object、RegExp 正则表达式、Date
返回一个和接收对象一样的数据,但是两者之间没有任何节点有联系,修改 A 不会影响 B

版本 0.1 最简单版本(直接一把梭)

直接使用 JSON 序列化

1
2
3
4
let a = { a: 1, b: 2, c: 3 }
let b = JSON.parse(JSON.stringify(a))
console.log(a)
console.log(b)

缺点:

1.不支持函数,比如对象里有有一个 key,value 的 value 是一个函数

2.不支持 undefined

3.JSON 只支持树状的结构不支持环状的结构,如果原对象有一个对自己的引用,JSON 的序列化会报错

4.不支持 Date、正则表达式、Symbol、Set、Map(综上就是所有 JSON 不支持的数据类型都不支持)

其他的浅拷贝办法:

1.Object.assign({}, a)

2.{…a}


版本 1 我能拷贝有不同对象的数据了

可以看到 JSON 序列化的深拷贝方式已经不满足了,
那么现在我们得手动实现一个深拷贝了,我们用递归的形式来实现

在递归的时候一定会遇到不同的数据类型,要按照不用的数据类型来分类讨论

js 一共有 8 种数据类型,
如果是 7 种基本类型之一就直接 return
undefined
null
boolean
number
string
symbol
bigint

如果是 object 就再分类讨论,不同的类型用不同的构造方法去构造一个新的对象

  1. Array 就遍历复制属性到一个新的数组 new Array
  2. function 就在一个新的 function 内 source.apply(this, arguments) //source 是要拷贝的对象
    再遍历 source 的属性到这个新的 function
  3. Object 也 遍历复制 key value 到一个新对象 new Object
  4. RegExp 正则表达式 数据类型
  5. Date 数据类型

其中所有的复制都用 deepClone 这个函数自己递归

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function deepClone(source) {
  if (source instanceof Object) {
    let dist
    if (source instanceof Array) {
      dist = new Array()
    } else if (source instanceof Function) {
      dist = function () {
        return source.apply(this, arguments)
      }
    } else if (source instanceof RegExp) {
      dist = new RegExp(source.source, source.flags)
    } else if (source instanceof Date) {
      dist = new Date(source)
    } else {
      dist = new Object()
    }

    for (let key in source) {
      dist[key] = deepClone(source[key])
    }
    return dist
  }
  return source
}

版本 2 我能拷贝具有环结构的数据了

到目前为止一切顺利,也没有什么大问题

因为要拷贝的对象的数据是有限的 deepClone 的递归会自动结束,不用特地给一个出口

但是考虑到一种情况,在讨论 JSON 序列化深拷贝的时候第三种情况是拷贝环结构,
即一个对象的某个属性引用了自身,比如:window.self === window

我们目前构建的 deepClone 方法,在遇到有环结构的对象时会不断递归永远没有出口结束
数组也可能存在环结构:

1
a[0] = a

函数也可能存在环结构:

1
2
3
4
function x() {
  return x()
}
x()

解决办法是使用缓存来标记已经复制过的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
let cache = []

function deepClone(source) {
  if (source instanceof Object) {
    let cacheDist = findCache(source)
    // 有缓存
    if (cacheDist) {
      return cacheDist
    } else {
      //无缓存
      let dist
      if (source instanceof Array) {
        dist = new Array()
      } else if (source instanceof Function) {
        dist = function () {
          return source.apply(this, arguments)
        }
      } else if (source instanceof RegExp) {
        dist = new RegExp(source.source, source.flags)
      } else if (source instanceof Date) {
        dist = new Date(source)
      } else {
        dist = new Object()
      }

      // 加入缓存
      cache.push([source, dist])
      for (let key in source) {
        dist[key] = deepClone(source[key])
      }
      return dist
    }
  }
  // 如果是基本类型就直接return
  return source
}

// 查看当前的对象是否已经在缓存中
function findCache(source) {
  for (let i = 0; i < cache.length; i++) {
    if (cache[i][0] === source) {
      return cache[i][1]
    }
  }
  return undefined
}

版本 3 我需要跳过原型再拷贝

现在具有环结构的对象现在也可以复制了。

此外还有一个坑,我们知道每一个对象都有自己的原型,__proto__属性
目前还没考虑是否要拷贝对象的原型(这里原型很可能还有原型是很深的一个对象属性)
一般来说我们不需要拷贝 proto 这个属性,因为原型里的所有方法和对象都有各自的原型,如果我们要决定拷贝原型
就意味着要把 js 中的很多基本原型都拷贝一遍,所以这里一般不需要拷贝对象的原型,所以需要绕开它

1
2
3
4
5
6
for (let key in source) {
  // 只复制对象自己的属性,跳过原型的属性
  if (source.hasOwnProperty(key)) {
    dist[key] = deepClone(source[key])
  }
}

我们修改一下代码,下面是完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
let cache = []

function deepClone(source) {
  if (source instanceof Object) {
    let cacheDist = findCache(source)
    // 有缓存
    if (cacheDist) {
      return cacheDist
    } else {
      //无缓存
      let dist
      if (source instanceof Array) {
        dist = new Array()
      } else if (source instanceof Function) {
        dist = function () {
          return source.apply(this, arguments)
        }
      } else if (source instanceof RegExp) {
        dist = new RegExp(source.source, source.flags)
      } else if (source instanceof Date) {
        dist = new Date(source)
      } else {
        dist = new Object()
      }

      // 加入缓存
      cache.push([source, dist])
      for (let key in source) {
        // 只复制对象自己的属性,跳过原型的属性
        if (source.hasOwnProperty(key)) {
          dist[key] = deepClone(source[key])
        }
      }
      return dist
    }
  }
  // 如果是基本类型就直接return
  return source
}

// 查看当前的对象是否已经在缓存中
function findCache(source) {
  for (let i = 0; i < cache.length; i++) {
    if (cache[i][0] === source) {
      return cache[i][1]
    }
  }
  return undefined
}

版本 4 强化拷贝函数,最后的力量!

你或许有注意到,目前在函数外面有一个 cache 用于存放深拷贝时的缓存,我们在调用完后还得清空它这样很麻烦

我们尝试写成一个 class,来包含 cache 变量和帮助函数 findCache

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class DeepClone {
  cache
  constructor() {
    this.cache = []
  }
  clone(source) {
    if (source instanceof Object) {
      let cacheDist = this.findCache(source)
      // 有缓存
      if (cacheDist) {
        return cacheDist
      } else { //无缓存
        let dist
        if (source instanceof Array) {
          dist = new Array()
        } else if (source instanceof Function) {
          dist = function () {
            return source.apply(this, arguments)
          }
        } else if (source instanceof RegExp) {
          dist = new RegExp(source.source, source.flags)
        } else if (source instanceof Date) {
          dist = new Date(source)
        } else {
          dist = new Object()
        }

        // 加入缓存
        this.cache.push([source, dist])
        for (let key in source) {
          // 只复制对象自己的属性,跳过原型的属性
          if (source.hasOwnProperty(key)) {
            dist[key] = this.clone(source[key])
          }
        }
        return dist
      }
    }
    // 如果是基本类型就直接return
    return source
  }

  // 查看当前的对象是否已经在缓存中
  findCache(source) {
    for (let i = 0; i < this.cache.length; i++) {
      if (this.cache[i][0] === source) {
        return this.cache[i][1]
      }
    }
    return undefined
  }
}

const a = {
  self: a
  child: [
    a: undefined,
    b: new Date()
    c: new RegExp("hi\\d+", "gi")
  ]
}
const b = new DeepClone().clone(a)

什么?最后的魔王

有一种情况无论是在浏览器还是在 nodejs 环境中都会存在
当对象有子属性,子属性还有子属性。。。对象的结构非常深的时候会导致递归调用很多次,会出现函数执行栈超出最大值的情况
(爆栈:RangeError: Maximum call stack size exceeded)

这里设想的办法是把对象的纵向的结构换成横向的结构(放在类似数组的结构里)这样用 for 循环来避免递归过于深的情况
暂时不知道怎么解


我选择真香

自己写的 deepClone 总会有考虑不周全的地方
实际工作中可以借助第三方:
// Lodash
Lodash.cloneDeep()

// 干脆不用深拷贝了,使用部分修改
immutable.js 部分修改
immer.js

分享

Llane00
作者
Llane00
Web Developer