深拷贝的终极二次探索(99%的前端都不知道)

1. 前言

很多前端er认为深拷贝很难,其实我觉得主要是网上很多文章代码虽然实现了深拷贝,但代码冗余度很高,确实是不利于阅读。

2. 难点在哪?

首先我给出下面这个对象

var obj = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45",
        b: {
            name: '小b',
            age: 18,
            next: {
                name: '小二'
            }
        }
    },
    a1: undefined,
    a2: null,
    a3: 123
}

深拷贝与浅拷贝的区别就在于层级,浅拷贝只会复制一层的属性,深拷贝会完整的复制整个对象

我们不考虑任何特殊情况,实现一个简单的深拷贝(其实就是DFS遍历)

function cloneDeep(obj) {
    var res = {}
    for(var key in obj) {
        if (obj.hasOwnProperty(key)) {
            var value = obj[key]
            if (typeof(value) == 'object') {
                res[key] = cloneDeep(value)
            } else {
                res[key] = value
            }
        }
    }
    return res
}

这个函数是有问题的,遍历我们给出的obj对象时,a2: null复制失败了,因为我们判断对象的函数很粗糙

引出第一个问题:
复制一些特殊对象(像Function,Date,Regexp…等)时要单独处理,对理解算法没有太大帮助

所以我们只对普通的对象进行处理

// 更精确的判断object函数
function isObject (obj) {
    return Object.prototype.toString.call(obj) == '[object Object]'
}

然后我们看看还存在了什么问题?

然后看看其他比较常见的问题

  1. 递归爆栈
  2. 引用丢失
  3. 闭环

3. 递归太深了

当递归的层级过深的时候,会出现栈溢出的危险,一般我们考虑使用循环来代替递归来解决栈溢出的问题

先不管怎么去用循环重构我们的深拷贝函数

思考一下,深拷贝函数设计的流程是怎么样的?

只有两个步骤

  1. 遍历对象
  2. 遍历的过程中拷贝

ok,那按照步骤来,我们试着用循环去遍历对象,其实这就是BFS,广度优先搜索

其实与二叉树的层序遍历异曲同工,只是我们的对象不只两个节点,而是多个节点

下面的代码均采用ES6新语法

// BFS遍历对象
const cloneDeep = (obj) => {
    // q是一个队列结构,实现BFS的基础
    const q = [obj]
    while (q.length > 0) {
        const t = q.shift()
        Object.keys(t).map(key => {
            const value = t[key]
            // console.log(`${key}: ${value}`)
            if (isObject(value)) {
                q.push(value)
            } 
        })
    }
    return res
}

ok,遍历对象完成了,那怎么在遍历对象的时候,将拷贝对象的属性呢?

关键点在于我们怎么去复制边,这个边就是我们两个节点,原节点和克隆节点的映射关系

我们通过Map来映射我们的节点关系

在es6中有一个新的数据结构叫Map,多数静态语言中都会有,因为js中的object实质上就是一个类Map的结构,但是它的键值只能存字符串,所以它不能很好的描述两个节点的映射关系

const cloneDeep = (obj) => {
    // res是我们的clone节点
    const res = {}
    const q = [obj]
    const map = new Map()
    // 建立对象节点与克隆节点的映射关系
    map.set(obj, res)
    while (q.length > 0) {
        const t = q.shift()
        Object.keys(t).map(key => {
            const value = t[key]
            if (isObject(value)) {
                // 建立这个属性对象与克隆节点的映射关系
                map.set(value, {})
                q.push(value)
            } 
            // 找到对应的克隆节点
            const clone = map.get(t)
            clone[key] = value
        })
    }
    return res
}

这个深拷贝就解决了我们的递归爆栈问题,它其实并不难,相对于遍历树这步的代码而言,我们只增加了三行代码就解决了拷贝这一问题

4.引用丢失怎么办

引用丢失问题,obj.aobj.c都引用到了c对象

就像下面这样

var obj = {
    a: {
       name: c
    }
    b: c
}

那是不是会丢失引用呢?当然不会,引用本就不可能丢失,只是大多数人写的深拷贝函数有问题,需要额外的来增加代码解决引用丢失问题。

5. 闭环

闭环是个啥子情况呢?是指对象的引用链中,有一个属性引用到了之前的对象,于是引用链成了一个环形结构,也就是闭环

var a = {
    b: {}
}
a.b.c = a

在这里插入图片描述
这种情况也很好解决,我们在更新映射关系的时候,我们加个判断,如果原节点在map中能找到,就不用创建新的克隆对象

最终版代码

const cloneDeep = (obj) => {
    // res是我们的clone节点
    const res = {}
    const q = [obj]
    const map = new Map()
    // 建立对象节点与克隆节点的映射关系
    map.set(obj, res)
    while (q.length > 0) {
        const t = q.shift()
        Object.keys(t).map(key => {
            const value = t[key]
            if (isObject(value)) {
                // 如果对象已经存在在表中,说明存在循环引用
                if (map.has(value)) {
                    map.set(t, value)
                } else {
                    // 建立这个属性对象与克隆节点的映射关系
                    map.set(value, {})
                    q.push(value)
                }
            } 
            // 找到对应的克隆节点
            const clone = map.get(t)
            clone[key] = value
        })
    }
    return res
}

6. 最后

其实深拷贝下来,比较重要的就两个点

怎么遍历对象,怎么建立原节点和克隆节点之间的联系

最终一个简易的深拷贝实现下来也就二十行代码左右

写作不易~~如果对你有帮助的话,点个赞吧,Thanks♪(・ω・)ノ

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值