手写深拷贝 DeepClone

2,690 阅读4分钟

关于深拷贝

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

深拷贝是前端面试的必考题之一,那么今天就来学习手写一个深拷贝函数吧!

什么是深拷贝?

首先先来看两个小问题

问题一:

let a = 1
b = a 
b = 2

// a = ?

答案:a = 1

问题二:

let a = { name: 'test' }
b = a
b.age = 18

// a.age = ?

答案:a.age = 18

为什么会这样呢?

出现这样的原因,是因为 js 的 Object 类型的赋值是一个引用,也就是说 a = {name: 'test'} 这个 a 其实指向的是 {name: 'test'} 这个对象的地址。而 b = a 这个操作,就是把 a 指向的地址赋值给 b,那么 a、b 就会指向同一个地址,也就都指向了 {name: 'test'} 这个对象。 而这样的一个复制拷贝的过程,也就是浅拷贝

那么如果我想要修改 b 对象,但是不修改 a 对象的值,应该怎么做呢? 这就需要重新开辟一个内存空间,用来存放 b 指向的对象,这一过程就是深拷贝。

如何实现深拷贝?

简易方法:

最简单,也是工作中使用最多的,能解决大部分问题的方法,JSON 序列化

const a = {name: 'test'}
const b = JSON.parse(JSON.stringify(a))

b.age = 18
console.log(a.age)// undefined

但是这个方法也会有一个问题,如果 a 对象中的 value 存在函数、undefined、Date、RegExp,那么 JSON 序列化的结果就会丢失、转化成另一个格式的内容等结果,原因是 JSON 不支持这些数据类型。也就从这里,引出了手写一个深拷贝的需求。 示例:

image.png

image.png

进阶方法

  • 首先先来写一个能够拷贝普通对象和基本类型的实现
class DeepClone {
    clone(source) {
        if (source instanceof Object) {
            let dist = {}
            for(let key in source) {
                dist[key] = this.clone(source[key])
            }
            return dist
        } else {
            return source
        }
    }
}

这里我们编写一个简单的测试用例,确保这个方法是正常的

describe('DeepClone', () => {
  it('他是一个类', () => {
    assert.isFunction(DeepClone)
  })
  it('能够复制基本类型', () => {
    const number = 123
    const deepClone = new DeepClone()
    const number2 = deepClone.clone(number)
    assert(number === number2)
    const string = '123456'
    const string2 = deepClone.clone(string)
    assert(string === string2)
    const boolean1 = true
    const boolean2 = deepClone.clone(boolean1)
    assert(boolean1 === boolean2)
    const u = undefined
    const u2 = deepClone.clone(u)
    assert(u === u2)
    const empty = null
    const empty2 = deepClone.clone(empty)
    assert(empty === empty2)
    const symbol = Symbol()
    const symbol2 = deepClone.clone(symbol)
    assert(symbol === symbol2)
  })
  describe('对象', () => {
    it('能够复制普通对象', () => {
      const a = {name: 'wwwbh', child: {name: 'xwwwbh', age: 1}}
      const deepClone = new DeepClone()
      const a2 = deepClone.clone(a)
      assert(a !== a2)
      assert(a.name === a2.name)
      assert(a.child !== a2.child)
      assert(a.child.name === a2.child.name)
      assert(a.child.age === a2.child.age)
    })
  })
})

此时运行测试可以看到,我们函数复制普通类型和简单对象都是没有问题的 1632982329(1).png

  • 接下来加上 Array、Function、RegExp、Date 的类型
class DeepClone {
    clone(source) {
        if (source instanceof Object) {
            let dist
            if (source instanceof Array) {
                dist = []
            } else if (source instanceof Function) {
                dist = function() {
                    return source.call(this, ...arguments)
                }
            } else if (source instanceof Date) {
                dist = new Date(source)
            } else if (source instanceof RegExp) {
                dist = new RegExp(source.source, source.flags)
            } else {
                dist = {}
            }
            for(let key in source) {
                dist[key] = this.clone(source[key])
            }
            return dist
        } else {
            return source
        }
    }
}

完善测试用例:

describe('DeepClone', () => {
    // ...
    describe('对象', () => {
      it('能够复制普通对象', () => {
        const a = {name: 'wwwbh', child: {name: 'xwwwbh', age: 1}}
        const deepClone = new DeepClone()
        const a2 = deepClone.clone(a)
        assert(a !== a2)
        assert(a.name === a2.name)
        assert(a.child !== a2.child)
        assert(a.child.name === a2.child.name)
        assert(a.child.age === a2.child.age)
      })
      it('能够复制数组对象', () => {
        const a = [[11, 12], [21, 22], [31, 32]]
        const deepClone = new DeepClone()
        const a2 = deepClone.clone(a)
        assert(a !== a2)
        assert(a[0] !== a2[0])
        assert(a[1] !== a2[1])
        assert(a[2] !== a2[2])
        assert.deepEqual(a, a2)
      })
      it('能够复制函数对象', () => {
        const a = function (x, y) {
          return x + y
        }
        a.xxx = {yyy: {zzz: 1}}
        const deepClone = new DeepClone()
        const a2 = deepClone.clone(a)
        assert(a !== a2)
        assert(a.xxx !== a2.xxx)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
        assert(a(1, 2) === a2(1, 2))
      })
      it('可以复制正则', () => {
        const a = new RegExp('hi\d+', 'gi')
        a.xxx = {yyy: {zzz: 1}}
        const deepClone = new DeepClone()
        const a2 = deepClone.clone(a)
        assert(a.source === a2.source)
        assert(a.flags === a2.flags)
        assert(a.xxx !== a2.xxx)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
      })
      it('可以复制日期', () => {
        const a = new Date()
        a.xxx = {yyy: {zzz: 1}}
        const deepClone = new DeepClone()
        const a2 = deepClone.clone(a)
        assert(a !== a2)
        assert(a.getTime() === a2.getTime())
        assert(a.xxx !== a2.xxx)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
      })
    })
})

此时运行我们的测试用例

1632983000(1).png

再次进阶

目前我们的 DeepClone 还不支持环引用,也就是这样的数据类型:

image.png

如果把这样的数据类型放到我们的 DeepClone 中,无疑会触发递归死循环,直接爆栈。

为了解决这个问题,我们需要一个 cache 来存储每次传递进来的 source,同时拿这个source来与 cache 做比较,是否存在引用相同地址的对象,如果没有,就放入 cache 中,如果存在,就直接返回。由此来解决环引用的问题。

class DeepClone {
  cache = []
  clone(source) {
    if (source instanceof Object) {
      let dist
      const cacheDist = this.findCache(source)
      if (cacheDist){
        return cacheDist
      } else {
        if (source instanceof Array) {
          dist = []
        } else if (source instanceof Function) {
          dist = function() {
            return source.call(this, ...arguments)
          }
        } else if (source instanceof Date) {
          dist = new Date(source)
        } else if (source instanceof RegExp) {
          dist = new RegExp(source.source, source.flags)
        } else {
          dist = {}
        }
        this.cache.push([source, dist])
        for(let key in source) {
          dist[key] = this.clone(source[key])
        }
        return dist
      }
    } else {
      return source
    }
  }

  findCache(source){
    for(let i = 0; i < this.cache.length; i ++) {
      if (this.cache[i][0] === source) {
        return this.cache[i][1]
      }
    }
  }
}

完善测试用例:

describe('DeepClone', () => {
    // ...
    describe('对象', () => {
         //...
        it('能够复制环引用的对象', () => {
          const a = {name: 'wwwbh'}
          a.self = a
          const deepClone = new DeepClone()
          const a2 = deepClone.clone(a)
          assert(a !== a2)
          assert(a.name === a2.name)
          assert(a.self !== a2.self)
        })
    })
})

此时再次运行测试

1632984729(1).png

那么环引用对象的深拷贝,也被我们解决了

最后的进化

最后一点,一般来说,深拷贝不需要复制原型链上的方法对象,那么就使用 hasOwnProperty 方法来做个判断。

最终代码

class DeepClone {
  cache = []
  clone(source) {
    if (source instanceof Object) {
      let cachedDist = this.findCache(source)
      if (cachedDist) {
        return cachedDist
      } else {
        let dist
        if (source instanceof Object) {
          if (source instanceof Array) {
            dist = []
          } else if (source instanceof Function) {
            dist = function () {
              return source.call(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 = {}
          }
          this.cache.push([source, dist])
          for (let key in source) {
            if(source.hasOwnProperty(key)){
              // 只复制不在原型上的属性
              dist[key] = this.clone(source[key])
            }
          }
          return dist
        }
      }
    }
    return source
  }

  findCache(source) {
    for (let i = 0; i < this.cache.length; i++) {
      if (this.cache[i][0] === source) {
        return this.cache[i][1]
      }
    }
  }
}

源码链接

github.com/wbh13285517…