手写源码系列 — 手写深拷贝

723 阅读6分钟

什么是深拷贝

  • 简单理解 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 d64539493baf764462c4e0e48464a384.png 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.继续处理栈的数据,直到栈为空。

源码地址