深拷贝实现

62 阅读2分钟

加入我们要拷贝如下的对象:

const data = {
  name: '巴卡巴卡',
  age: 12,
  ext: {
     color: 'red',
     cover: {
        count: 3
     }
  }
}

首先是递归的方式实现

function deepClone1(obj) {

    if(obj === null || typeof obj !== 'object') {
        return obj
    }

    const copy = Array.isArray(obj) ? [] : {}

    Object.keys(obj).forEach(key => {
        copy[key] = deepClone(obj[key])
    })

    return copy
}

测试:

image.png

但是如果源数据递归引用,深拷贝就会报 Uncaught RangeError: Maximum call stack size exceeded 栈溢出错误

我们修改下,实现递归引用:

function deepClone(obj, hash = new WeakMap()) {

    if(obj === null || typeof obj !== 'object') {
        return obj
    }

    if (hash.has(obj)) {
        return hash.get(obj)
    }

    const copy = Array.isArray(obj) ? [] : {}

    hash.set(obj, copy)

    Object.keys(obj).forEach(key => {
        copy[key] = deepClone(obj[key], hash)
    })

    return copy
}

这里用 WeakMap 类型作为缓存,如果存在循环引用,就返回引用自身,不进行拷贝。WeakMap 的键必须是对象,且为弱引用,当没有其他引用指向缓存中的键对象时,这些对象以及它们对应的值会被垃圾回收机制自动清理,有助于避免不必要的内存占用,并简化缓存管理。

到这里深拷贝和处理循环拷贝就全部实现了。

接下来我们不用递归的方式,用循环来模拟递归实现深拷贝: 对象可以看做一棵树

    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1       

用循环遍历一棵树,借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点,首先我们往栈里放入种子数据,key用来存储放哪一个父元素的哪一个子元素拷贝对象,然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝。

function cloneLoop(x) {
   const root = {}

    const loopList = [{
        parent: root,
        key: undefined,
        data: x
    }]

    while( loopList.length) {

        // 深度优先遍历
        const node = loopList.pop()
        const parent = node.parent
        const key = node.key
        const data = node.data

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent

        if (typeof key !== 'undefined') {
            parent[key] = {}
            res = {}
        }

        Object.keys(data).forEach(key => {

            if (typeof data[key] === 'object') {

                loopList.push({
                    parent: res,
                    key,
                    data: data[key]
                })
            } else {
                res[key] = data[key]
            }
        })
        
    }

    return root
}

上面的实现不能处理循环引用,我们继续:

function cloneLoop(x) {
   const root = {}

   const hash = new WeakMap()

   const loopList = [{
        parent: root,
        key: undefined,
        data: x
    }]

    while( loopList.length) {
        // 深度优先遍历
        const node = loopList.pop()
        const parent = node.parent
        const key = node.key
        const data = node.data

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent

        if (typeof key !== 'undefined') {
            parent[key] = {}
            res = {}
        }

        if (hash.has(data)){
            parent[key] = hash.get(data)
            break;
        }

        hash.set(data, res)

        Object.keys(data).forEach(key => {

            if (typeof data[key] === 'object') {
                loopList.push({
                    parent: res,
                    key,
                    data: data[key]
                })
            } else {
                res[key] = data[key]
            }
        })
        
    }
    return root    
}

深拷贝的终极实现