循序渐进的实现一个较为完整的深拷贝

本文涉及的产品
对象存储 OSS,标准 - 本地冗余存储 20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000 次 1年
简介: 循序渐进的实现一个较为完整的深拷贝

简介


对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况,解决这种问题的方法就是利用深/浅拷贝

浅拷贝可以直接使用扩展运算符(...)或者Object.assign直接处理

而深拷贝就没有现成的函数了

在业务开发中通常使用 JSON.parse(JSON.stringify(data)) 应付大多数的深拷贝场景

但面试就一般考察使用递归去实现一个深拷贝

本文将较为详细的介绍每一种边界情况的实现方案


常见边界问题


  • 循环引用
  • 函数
  • 正则
  • 日期
  • symbol
  • 多个键值引用了同一个对象,保持拷贝后的特性一致
  • ...


1. 简单递归实现


不考虑边界问题,元素只有 值类型,obj,arr


function deepClone(obj) {
    if (!isObject(obj)) return obj
    if (Array.isArray(obj)) {
        const newObj = []
        for (const v of obj) {
            newObj.push(isObject(v) ? deepClone(v) : v)
        }
        return newObj
    }
    if (isObject(obj)) {
        const newObj = {}
        Object.keys(obj).forEach(k => {
            const v = obj[k]
            newObj[k] = isObject(v) ? deepClone(v) : v
        })
        return newObj
    }
}
const a = {
    name: 'xiaoming', id: 123131, info: {
        bd: '2020-01-01',
        cards: [{
            q: 'q',
            w: [1, 2, 3],
            e: { c: 'c' }
        }]
    }
}
console.log(JSON.stringify(deepClone(a)));


2. 解决循环引用


众所周知 使用JSON进行深拷贝是无法解决对象的循环引用,如果出现了会直接报错


可以使用哈希表来解决此问题,将已存在的对象记录下来


对上面的deepclone稍加改动


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 解决循环引用
        if (map.has(obj)) return map.get(obj)
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            Object.keys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            return newObj
        }
    }
    return dp(obj)
}
const b = {}, c = {}
b.next = c
c.next = b
console.log(deepClone(b)); // { next: { next: {} } }


3. 保持原对象的引用的特性


  • 将已拷贝后的对象存储起来
  • 经clone过的对象直接返回


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            Object.keys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const obj = { a: 1 }
const t3 = { a: obj, d: obj, f: { g: obj } }
const tt3 = deepClone(t3)
console.log(tt3); // { a: { a: 1 }, d: { a: 1 }, f: { g: { a: 1 } } }
console.log(tt3.a === tt3.d); // true
console.log(tt3.a === tt3.f.g); // true


4. 拷贝Symbol


这里最主要的是如何获取到到对象的Symbol键


获取对象的键的方案有以下几种


  • Reflect.ownKeys(target): 方法返回一个由目标对象自身的属性键组成的数组(包含普通键与Symbol键)
  • Object.getOwnPropertySymbols(target):返回一个给定对象自身的所有 Symbol 属性的数组
  • Object.getOwnPropertyNames(target):返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性不包括Symbol值作为名称的属性)组成的数组
  • Object.keys():返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致


综上


Reflect.ownKeys(target) 
// 等价于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))


再稍加改动一下我们的deepClone方法,这里我们直接采用Reflect.ownKeys(target)替代原来的Object.keys(targer)


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const s1 = Symbol.for('s1')
const s2 = Symbol.for('s2')
const data = {
    [s1]: {
        name: 's1',
        age: 19
    },
    [s2]: [1, 2, 'string', {
        title: s1
    }]
}
console.log(deepClone(data));
// { [Symbol(s1)]: { name: 's1', age: 19 },
//   [Symbol(s2)]: [ 1, 2, 'string', { title: Symbol(s1) } ] }


5. 拷贝特殊对象Date/RegExp


对于特殊的对象,我们可以通过以下几步去处理

  • 获取对象的构造函数
  • 判断是否是指定的特殊对象
  • 调用构造函数生成一个新的对象


实例化的对象可以通过.constructor获取到其构造函数

我们修改上面的clone方法


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        // 获取对象的构造函数
        const fn = obj.constructor
        // 如果是正则
        if (fn === RegExp) {
            return new RegExp(obj)
        }
        // 如果是日期
        if (fn === Date) {
            return new Date(obj.getTime())
        }
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const data = {
    today: new Date(),
    reg: /^abc$/ig
}
console.log(deepClone(data)); // { today: 2020-09-01T08:45:26.907Z, reg: /^abc$/gi }


拷贝函数


函数拷贝的方案在网上收集了一下五花八门,各种奇淫技巧,下面给大家列举一下哈哈


  1. 使用eval:
  • eval(fn.toString()):只支持箭头函数
  • new Function(‘return’+fn.toString())():不能将函数及其原始作用域一起克隆


  1. fn.bind():返回的新函数无法再通过bind去改变this指向


// 我这里就简单的使用.bind
function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        // 获取对象的构造函数
        const fn = obj.constructor
        // 如果是正则
        if (fn === RegExp) {
            return new RegExp(obj)
        }
        // 如果是日期
        if (fn === Date) {
            return new Date(obj.getTime())
        }
        // 如果是函数
        if (fn === Function) {
            return obj.bind({})
        }
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const data = {
    today: new Date(),
    reg: /^abc$/ig,
    fn1: (a, b) => {
        console.log(this.today);
        console.log(a + b);
    },
    fn2: function (a, b) {
        console.log(this.reg);
        console.log(a + b);
    }
}
const newData = deepClone(data)
newData.fn1(1, 2) // undefined 3
newData.fn1.call({ today: '666' }, 1, 2) // undefined 3
newData.fn2(3, 4) // /^abc$/gi  7
newData.fn2.call({ reg: 123 }, 3, 4) // 123 7
const fn2 = newData.fn2
fn2.call({ reg: 'fn2Call' }, 2, 3) // fn2Call 5
const fn3 = fn2.bind({ reg: 'string' })
fn3(2, 3) // string 5


更详细的内容大家可以细品一下这篇文章


关于深拷贝完整实现 可以研究一下 lodash.cloneDeep源码


相关实践学习
通义万相文本绘图与人像美化
本解决方案展示了如何利用自研的通义万相AIGC技术在Web服务中实现先进的图像生成。
相关文章
|
8月前
|
机器学习/深度学习 人工智能 DataWorks
《混沌中寻序:DataWorks与人工智能解锁非结构化数据密码》
在数字化时代,非结构化数据如社交媒体文本、图像和监控视频等呈爆炸式增长,看似无序却暗藏规律。阿里云DataWorks借助人工智能算法,在这团“数据乱麻”中探寻秩序,挖掘潜在价值。通过机器学习和深度学习技术,DataWorks实现了特征提取、聚类分类等功能,高效处理海量复杂数据,为企业和社会创造巨大价值。这一过程犹如在混沌中发现有序,不断突破迷雾,开启智能未来。
139 1
|
Ubuntu 数据管理 Linux
Linux系统要求
【8月更文挑战第9天】Linux系统要求
649 1
|
云安全 弹性计算 Linux
【玩转阿里云游戏服务器】阿里云幻兽帕鲁游戏服务器搭建教程
【玩转阿里云游戏服务器】阿里云幻兽帕鲁游戏服务器搭建教程:在《幻兽帕鲁》中,玩家可以在一个广阔的世界中自由探索,并收集神奇的生物“帕鲁”。这些帕鲁生物各具特色,有的擅长战斗,有的适合进行农活和工业生产。游戏中的多人模式让你能够邀请朋友一同加入冒险,共同创造独特的游戏记忆,展开合作或对战。不少玩家不知道幻兽帕鲁服务器怎么搭建,接下来分享幻兽帕鲁搭建本地服务器教程 !
|
存储 API 容器
[ROS通信机制] --- 参数服务器
[ROS通信机制] --- 参数服务器
462 0
[ROS通信机制] --- 参数服务器
|
1天前
|
弹性计算 运维 搜索推荐
三翼鸟携手阿里云ECS g9i:智慧家庭场景的效能革命与未来生活新范式
三翼鸟是海尔智家旗下全球首个智慧家庭场景品牌,致力于提供覆盖衣、食、住、娱的一站式全场景解决方案。截至2025年,服务近1亿家庭,连接设备超5000万台。面对高并发、低延迟与稳定性挑战,全面升级为阿里云ECS g9i实例,实现连接能力提升40%、故障率下降90%、响应速度提升至120ms以内,成本降低20%,推动智慧家庭体验全面跃迁。
|
2天前
|
数据采集 人工智能 自然语言处理
3分钟采集134篇AI文章!深度解析如何通过云无影AgentBay实现25倍并发 + LlamaIndex智能推荐
结合阿里云无影 AgentBay 云端并发采集与 LlamaIndex 智能分析,3分钟高效抓取134篇 AI Agent 文章,实现 AI 推荐、智能问答与知识沉淀,打造从数据获取到价值提炼的完整闭环。
339 90
|
9天前
|
人工智能 自然语言处理 前端开发
Qoder全栈开发实战指南:开启AI驱动的下一代编程范式
Qoder是阿里巴巴于2025年发布的AI编程平台,首创“智能代理式编程”,支持自然语言驱动的全栈开发。通过仓库级理解、多智能体协同与云端沙箱执行,实现从需求到上线的端到端自动化,大幅提升研发效率,重塑程序员角色,引领AI原生开发新范式。
806 156
|
2天前
|
数据采集 缓存 数据可视化
Android 无侵入式数据采集:从手动埋点到字节码插桩的演进之路
本文深入探讨Android无侵入式埋点技术,通过AOP与字节码插桩(如ASM)实现数据采集自动化,彻底解耦业务代码与埋点逻辑。涵盖页面浏览、点击事件自动追踪及注解驱动的半自动化方案,提升数据质量与研发效率,助力团队迈向高效、稳定的智能化埋点体系。(238字)
243 156