最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写源码系列 — 手写深拷贝

    正文概述 掘金(lzb302009)   2021-01-22   619

    什么是深拷贝

    • 简单理解

    b是a的一份拷贝,b中没有对a中对象的引用

    • 另一种理解

    b是a的一份拷贝,对b的修改不会影响到a

    解决方案

    1.JSON序列化反序列化

    const cloneTarget = JSON.parse(JSON.stringify(target))
    

    缺点:

    • 不支持函数 (函数会被忽略)
    • 不支持undefined (undefined会被忽略)
    • 不支持循环引用
    • 不支持Date (会转成ISO8601字符串)
    • 不支持正则 (会转成空对象)
    • 不支持Symbol (忽略)

    2.递归克隆

    思路:

    • 递归

    看节点的类型,如果是基本类型则直接拷贝,如果是Object则分开讨论

    • object分为 普通对象 - for in 数组 array - Array 初始化 函数 function - 怎么拷贝?闭包? 日期 Date - 怎么拷贝 正则表达式 RegExp - 怎么拷贝

    实现

    接下来我们使用测试驱动开发来实现一个深拷贝

    环境准备

    • 创建目录
    mkdir deepClone
    cd deepClone
    yarn init -y
    
    • 引入chai、sinon和typescript
    yarn add chai mocha sinon sinon-chai 
    yarn add @types/chai @types/mocha @types/sinon @types/sinon-chai  ts-node typescript
    
    • 安装完后,修改package.json
    "scripts": {  "test": "mocha -r ts-node/register test/**/*.ts"},
    

    开始测试驱动开发

    创建两个文件:

    • src/index.ts 源码实现
    • test/index.ts 测试用例

    接下来开始测试驱动开发,我们先来写一个简单的测试用例

    • 验证deepClone是一个函数

    test/index.ts

    import sinonChai from "sinon-chai"
    import chai from 'chai'
    import {describe} from 'mocha'
    import deepClone from "../src";
    const assert = chai.assert;
    chai.use(sinonChai);
    describe('deepClone', () => {    
        it('是一个函数', () => {   
            assert.isFunction(deepClone)    
         })
    })
    

    执行命令yarn test,这时测试环境就会报错,因为我们还没开始写代码。 接下来我们开始写代码来通过这个测试用例 src/index.ts

    function deepClone(){}
    
    export default deepClone
    

    再执行yarn test,控制台就会提示测试用例通过。 这样,我们一个简单的测试驱动用例就完成了。 接下来我们由浅入深一步步实现深拷贝的代码。

    • 能复制基本类型

    在test/index.ts增加一个新的测试用例

    it("能够复制基本类型", () => {    
    const n = 1;    
    const n2 = deepClone(n)    
    assert(n === n2)    
    const s = '112'    
    const s2 = deepClone(s)    
    assert(s === s2)    
    const b = true    
    const b2 = deepClone(b)   
    assert(b === b2)    
    const u = undefined    
    const u2 = deepClone(u)    
    assert(u === u2)    
    const empty = null    
    const empty2 = deepClone(empty)    
    assert(empty === empty2)    
    const sym = Symbol()    
    const sym2 = deepClone(sym)    
    assert(sym === sym2)
    });
    

    运行yarn test,会提示用例未通过 实现代码 src/index.ts

    function deepClone(target){
        return target  // 普通类型只要直接返回即可
    }
    export default deepClone
    

    再运行yarn test,会提示用例已通过

    • 能复制普通对象

    增加一个新的测试用例,用来测试复制普通对象 test/index.ts

    describe('对象', () => {    
        it('能复制普通对象', () => {        
        const a = {name: 'lzb', address: {zipCode: '000000'}}        
        const a2 = deepClone(a)        
        assert(a !== a2)        // 对象是不相等的
        assert(a.name === a2.name)    // 普通属性相等    
        assert(a.address !== a2.address)  // 对象属性不相等
        assert(a.address.zipCode === a2.address.zipCode)    
     })
    

    代码实现 src/index.ts

    type Dictionary = {[key:string]:any}
    function deepClone(target:any){
        // 判断是否对象类型
        if(typeof target==='object'){
            let cloneTarget:Dictionary = {} 
            for(let key in target){
                // 遍历每个属性并返回复制后的值
                cloneTarget[key] = deepClone(target[key])
            }
            return cloneTarget
        }
        return target  // 普通类型只要直接返回即可
    }
    export default deepClone
    
    • 能复制数组对象

    test/index.ts

    it('能复制数组对象', () => {    
    const a = [[11, 12], [21, 22], [31, 32]]    
    const a2 = deepClone(a)    
    assert(a2 !== a)    
    assert(a2[0] !== a[0])    
    assert(a2[1] !== a[1])    
    assert(a2[2] !== a[2])    
    assert.deepEqual(a, a2)})
    

    src/index.ts

    ...
    if(target instanceof Object){    
    let cloneTarget:Dictionary    
    if(target instanceof Array){        
        cloneTarget = [] //如果是数组,则初始化为空数组    }else{        
        cloneTarget = {} //否则初始化为空对象    
    }    
    for(let key in target){        
        // 遍历每个属性并返回复制后的值        
        cloneTarget[key] = deepClone(target[key])    
    }    
    return cloneTarget
    }
    ...
    
    • 能复制函数

    增加测试用例 test/index.ts

    ...
    it('能复制函数', ()=>{    
    const a = function (a:number,b:number){return a+b}    const a2 = deepClone(a)    
    assert(a!==a2)    
    assert(a(1,2)===a2(1,2))
    })
    ...
    

    运行yarn test 手写源码系列 — 手写深拷贝 src/index.ts

    if(target instanceof Array){    
        cloneTarget = [] //如果是数组,则初始化为空数组
    }else if(target instanceof Function){    
        cloneTarget = function (this:any){        
            return target.apply(this, arguments)    
        }
    }else{    
        cloneTarget = {} //否则初始化为空对象
     }
    ...
    

    再运行yarn test,控制台显示通过

    • 能复制环

    增加测试用例

    type Dictionary = { [key: string]: any }
    ...
    it('能复制环', ()=>{    
    const a:Dictionary = {x:{y:111}}    
    a.self = a    
    const a2 = deepClone(a)    
    assert(a !== a2)    
    assert(a.x!==a2.x)    
    assert(a.x.y===a2.x.y)})
    

    写代码让测试用例通过

    type Dictionary = {[key:string]:any}
    // 用map来缓存访问过的对象
    const map = new Map()
    function deepClone(target:any){
        // 判断是否对象类型
        if(target instanceof Object){
            // 判断是否缓存过
            if(map.has(target)){
                // 有则直接返回
                return map.get(target)
            }else{
                let cloneTarget:Dictionary
                if(target instanceof Array){
                    cloneTarget = [] //如果是数组,则初始化为空数组
                }else if(target instanceof Function){
                    cloneTarget = function (this:any){
                        return target.apply(this, arguments)
                    }
                }else{
                    cloneTarget = {} //否则初始化为空对象
                }
                // 需要在递归之前缓存
                map.set(target,cloneTarget)
                for(let key in target){
                    // 遍历每个属性并返回复制后的值
                    cloneTarget[key] = deepClone(target[key])
                }
                return cloneTarget
            }
    
        }
        return target  // 普通类型只要直接返回即可
    }
    export default deepClone
    
    • 复制正则表达式

    test/index.ts

    it('复制正则表达式', () => {
        const a = new RegExp('hi\d+', 'ig')
        const a2 = deepClone(a)
        assert(a !== a2)
        assert(a.source === a2.source)
        assert(a.flags === a2.flags)
    })
    

    src/index.ts

    ...
    }else if(target instanceof RegExp){
        // 正则表达式对象会有两个属性,source和flags,分别对应构造函数的两个参数
        cloneTarget = new RegExp(target.source, target.flags)
    }
    ...
    
    • 复制日期

    test/index.ts

    it('复制日期', ()=>{
        const a = new Date()
        const a2:Date = deepClone(a)
        assert(a!==a2)
        assert(a.getTime()===a2.getTime())
    })
    

    src/index.ts

    ...
    else if(target instanceof Date){
        cloneTarget = new Date(target)
    }
    ...
    
    • 自动跳过原型属性

    test/index.ts

    it('跳过原型属性', ()=>{
        const a = Object.create({name:'a'})
        a.x = {yyy:1}
        const a2 = deepClone(a)
        assert(a!==a2)
        assert.isFalse('name' in a2)
        assert(a.x!==a2.x)
        assert(a.x.y===a2.x.y)
    })
    

    src/index.ts

    for(let key in target){
        // 过滤原型属性
        if(target.hasOwnProperty(key)) {
            // 遍历每个属性并返回复制后的值
            cloneTarget[key] = deepClone(target[key])
        }
    }
    
    • 复制很复杂的对象

    test/index.ts

    it("很复杂的对象", () => {
        const a = {
            n: NaN,
            n2: Infinity,
            s: "",
            bool: false,
            null: null,
            u: undefined,
            sym: Symbol(),
            o: {
                n: NaN,
                n2: Infinity,
                s: "",
                bool: false,
                null: null,
                u: undefined,
                sym: Symbol()
            },
            array: [
                {
                    n: NaN,
                    n2: Infinity,
                    s: "",
                    bool: false,
                    null: null,
                    u: undefined,
                    sym: Symbol()
                }
            ]
        };
        const a2 = deepClone(a);
        assert(a !== a2);
        assert.isNaN(a2.n);
        assert(a.n2 === a2.n2);
        assert(a.s === a2.s);
        assert(a.bool === a2.bool);
        assert(a.null === a2.null);
        assert(a.u === a2.u);
        assert(a.sym === a2.sym);
        assert(a.o !== a2.o);
        assert.isNaN(a2.o.n);
        assert(a.o.n2 === a2.o.n2);
        assert(a.o.s === a2.o.s);
        assert(a.o.bool === a2.o.bool);
        assert(a.o.null === a2.o.null);
        assert(a.o.u === a2.o.u);
        assert(a.o.sym === a2.o.sym);
        assert(a.array !== a2.array);
        assert(a.array[0] !== a2.array[0]);
        assert.isNaN(a2.array[0].n);
        assert(a.array[0].n2 === a2.array[0].n2);
        assert(a.array[0].s === a2.array[0].s);
        assert(a.array[0].bool === a2.array[0].bool);
        assert(a.array[0].null === a2.array[0].null);
        assert(a.array[0].u === a2.array[0].u);
        assert(a.array[0].sym === a2.array[0].sym);
    });
    
    • 不会爆栈
    it("不会爆栈", () => {
        const a = { child: null };
        let b:Dictionary = a;
        for (let i = 0; i < 10000; i++) {
            b.child = {
                child: null
            };
            b = b.child;
        }
        const a2 = deepClone(a);
        assert(a !== a2);
        assert(a.child !== a2.child);
    });
    

    src/index.ts

    type Dictionary = { [key: string]: any }
    // 用map来缓存访问过的对象
    const map = new Map()
    
    function deepClone(target: any) {
        const stack: any = []
        if (target instanceof Object) {
            // 把根节点入栈
            const root = {parent: null, key: null, value: target}
            stack.push(root)
            let current = stack.pop()
            let cloneTarget
            while (current) {
                const nodeValue = current.value
                // 根据对象类型不同创建不同的对象
                const temp: Dictionary = createObject(nodeValue)
                // 缓存对象
                map.set(nodeValue, temp)
                // 遍历属性
                for (let k in nodeValue) {
                    if (!nodeValue.hasOwnProperty(k)) continue
                    // 如果属性值已经缓存,则把缓存的值返回
                    if (map.has(nodeValue[k])) {
                        temp[k] = map.get(nodeValue[k])
                    } else if (nodeValue[k] instanceof Object) {
                        // 如果属性值是一个对象,则先入栈,后续再处理
                        stack.push({parent: temp, key: k, value: nodeValue[k]})
                    } else {
                        // 属性值是普通类型,则直接赋值
                        temp[k] = nodeValue[k]
                    }
                }
                // 父节点不存在则把临时变量赋值给cloneTarget,存在则把临时变量赋值给父节点的key
                current.parent ? (current.parent[current.key] = temp) : (cloneTarget = temp)
                // 处理下一个节点
                current = stack.pop()
            }
            return cloneTarget
        }
        return target
    
    }
    
    const createObject = (target: Object) => {
        let obj: Dictionary
        if (target instanceof Array) {
            obj = [] //如果是数组,则初始化为空数组
        } else if (target instanceof Function) {
            obj = function (this: any) {
                return target.apply(this, arguments)
            }
        } else if (target instanceof RegExp) {
            // 正则表达式对象会有两个属性,source和flags,分别对应构造函数的两个参数
            obj = new RegExp(target.source, target.flags)
        } else if (target instanceof Date) {
            obj = new Date(target)
        } else {
            obj = {} //否则初始化为空对象
        }
        return obj
    }
    
    export default deepClone
    

    思路:把递归改成用栈和循环的方式实现, 1.首先把根节点入栈, 2.然后遍历栈,如果栈有数据,则取出栈顶。 3.先创建一个临时的对象,然后遍历栈顶的对象属性,如果属性值是对象,则根据属性和属性值创建一个新的节点,并入栈。如果属性值是普通类型的则直接赋值。 4.继续处理栈的数据,直到栈为空。

    源码地址


    起源地下载网 » 手写源码系列 — 手写深拷贝

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元