手写深拷贝

方案1.序列化反序列化

var a = {name: 'lifa',age:18}
var b = JSON.parse(JSON.stringify(a))

上面的代码b就是a的深拷贝当我们修改b里面的值的时候,a的值不会跟着变化。

1.1.缺点

1). 不支持function
如果被深拷贝的对象里面有函数的话,我们深拷贝后的对象会直接将函数忽略

2). 不支持undefined
如果被深拷贝的对象里面有undefined,我们使用JSON对其深拷贝后,深拷贝后的对象也会将undefined忽略

3). 不支持循环引用(不能把自己赋值给自己内部的属性)

报错,JSON只支持竖状的结构不支持环状结构

4). 会把Date类型变成字符串

Date类型经过JSON深拷贝后变成了ISO8601格式的字符串

5). 不支持正则表达式

正则表达式经过JSON后会变成空对象

6). 不支持Symbol

方案2. 递归克隆

2.1 思路

  • 递归
    看节点的类型(7种)
    如果是基本类型直接拷贝
    如果是object就分情况讨论(普通object、数组array、函数function、日期Date)

2.2 步骤

创建目录
引入chai和sinon
开始驱动测试开发
测试失败=> 改代码 => 测试成功 =>加测试 => 测试失败

  • src/index.js
function deepCLone() {

}
module.exports =  deepCLone
  • test/index.js
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const deepCLone = require('../src/index')
chai.use(sinonChai)
const assert = chai.assert
const deepClone = require('../src/index')
describe('deepClone', () => {
    it('是一个函数', () => {
        assert.isFunction(deepCLone)
    })
})
  • package.json
{
  "name": "deep-clone",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "mocha test/**/*.js"
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "mocha": "^6.2.0",
    "sinon": "^7.4.1",
    "sinon-chai": "^3.3.0"
  }
}

运行yarn test

2.2.1. 对基本类型的拷贝

对于基本类型直接传什么就返回什么

function deepClone(source) {
  return source
}

单元测试

 it('可以复制基本类型', () => {
        const n = 123
        const n2 = deepClone(n)
        assert(n ===n2)
        const b = '123456'
        const b2 = deepClone(b)
        assert(b === b2)
        const c = undefined
        const c2 = deepClone(c)
        assert(c === c2)
        const d = null
        const d2 = deepClone(d)
        assert(d === d2)
        const e = true
        const e2 = deepClone(e)
        assert(e === e2)
        const f = Symbol()
        const f2 = deepClone(f)
        assert(f === f2)
    })
2.2.2. 对于普通对象的深拷贝

先判断传入的参数类型是不是对象,然后创建一个新的对象,遍历入参对象的每一项key,将每一项的值都进行深拷贝然后把当前项赋值给新的对象

if (source instanceof Object) {
        const deepObj = new Object()
        for (let key in source) {
            // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
            deepObj[key] = deepClone(source[key])
        }
        return deepObj
    }

单元测试

it('可以复制普通对象', () => {
        const a = { name: '立发', child: { name: '小立发'}}
        const a2 = deepClone(a)
        assert(a !== a2)
        assert(a.name === a2.name)
        assert(a.child !== a2.child)
        assert(a.child.name === a2.child.name)
    })
2.2.3. 对于数组对象的深拷贝
  if (source instanceof Object) {
        if (source instanceof Array) {
          const deepObj = new Array()
          for (let key in source) {
            // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
            deepObj[key] = deepClone(source[key])
          }
          return deepObj
        }
}

单元测试

it('可以复制数组对象', () => {
        const a = [[1, 2], [4, 5], [6, 7]]
        const a2 = deepClone(a)
        assert(a !== a2)
        assert(a[0] !== a2[0])
        assert(a[1] !== a2[1])
        assert(a[2] !== a2[2])
        assert.deepEqual(a, a2)
})
2.2.4. 对于函数对象的深拷贝

判断一个函数是不是另一个函数的深拷贝,首先函数也是一个对象所以它应该满足对象深拷贝的条件
1).深拷贝后的函数应该不等于源函数
2).深拷贝后的函数应该和源函数的基本类型的属性相等
3).深拷贝后的函数应该和源函数的引用类型的属性不相等
其次针对于函数对象本身
4).深拷贝后的函数的执行结果应该和源函数的执行结果相等
针对上面四点的单元测试如下:

it('可以复制函数', () => {
        const a = function(x, y) {
            return x + y
        }
        a.xxx = { yyy: { zzz: 1 } }
        const a2 = deepClone(a)
        assert(a !== a2) //上面的1)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz) //2)
        assert(a.xxx.yyy !== a2.xxx.yyy) //3)
        assert(a.xxx !== a2.xxx) //3)
        assert(a(1,2) === a2(1,2)) //4)
    })
  • index.js
else if (source instanceof Function) {
     deepObj = function() {
         return source.apply(this, arguments)
      }
      for (let key in source) {
        // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
         deepObj[key] = deepClone(source[key])
      }
      return deepObj
 } 

实际上就是声明一个新的函数,然后在这个新的函数里把你传入的函数调用一遍(把当前新的函数的this传给传入的函数,然后把参数放进去)然后再return它

2.2.5. 对于环的深拷贝

我们上面的代码如果出现环引用(某个属性的值等于本身的引用地址)的话会出现死循环现象,所以我们需要先把我们每次的值存下来(以此来确定某个属性的值有没有被深拷贝过),如果下次调用发现某个属性的值已经深拷贝过了就直接返回第一次深拷贝过的值,否则就继续递归。

上面图中黑色的环路是我们传入的需要深拷贝的源对象,红色的环路是我们深拷贝后的。我们看到了一个1就把1直接存储下来(不经过深拷贝处理,如果是引用类型直接把引用地址存储下来),然后对1进行深拷贝生成一个新的1',然后看到2把2直接存储下来,然后对2进行深拷贝生成一个新的2',然后走到我们的3后面我们应该接的实际上是我们第一次拷贝好的1',所以就需要我们每次存储的时候把我们一开始的原始值和深拷贝后的值都存储下来,通过原始值来得到我们第一次深拷贝后的值

let cache = []
function deepClone(source) {
    if (source instanceof Object) {
        let cacheDist = findCache(source)
        if (cacheDist) {
            return cacheDist
        } else {
            let deepObj
            if (source instanceof Array) {
                deepObj = new Array()
            } else if (source instanceof Function) {
                deepObj = function() {
                    return source.apply(this, arguments)
                }
            } else {
                deepObj = new Object()
            }
            cache.push([source, deepObj])
            for (let key in source) {
                // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
                deepObj[key] = deepClone(source[key])
            }
            return deepObj
        }
    }
    return source
}
function findCache(source) {
    for (let i = 0; i < cache.length; i++) {
        if (cache[i][0] === source) {
            return cache[i][1]
        }
    }
    return undefined
}

单元测试

it('环也能复制', () => {
        const a = { name: '立发'}
        a.self = a
        const a2 = deepClone(a)
        assert(a !== a2)
        assert(a.name === a2.name)
        assert(a.self !== a2.self)
})
2.2.6. 正则表达式的深拷贝

我们对正则表达式的声明主要有两部分,一部分是它的文本,一部分是它的标志

const a = new RegExp('hi\\d+', 'gi')

上面的hi\d+就是它的文本,gi就是标志,所以我们要对它进行深拷贝的话只需要拿到它的文本和标志然后放到一个新的RegExp对象里即可,那么我们如何拿到这两部分的值那,通过a.source就可以拿到它的文本值,a.flags就可以拿到标志的值

if (source instanceof Object) {
  else if (source instanceof RegExp) {
        deepObj = new RegExp(source.source, source.flags)
   } 
}

单元测试

it('可以复制正则表达式', () => {
        const a = new RegExp("hi\\d+", 'gi')
        a.xxx = { yyy: { zzz: 1} }
        const a2 = deepClone(a)
        assert(a.source === a2.source)
        assert(a.flags === a2.flags)
        assert(a !== a2)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx !== a2.xxx)
})
2.2.6. Date类型的深拷贝

我们只需要判断如果是Date类型,就直接把当前的Date放到一个新的Date对象里即可,我们想要判断两个Date的值是否相等只需要通过getTime()方法就可以

if (source instanceof Object) {
  else if (source instanceof Date) {
        deepObj = new Date(source)
   } 
}

单元测试

it('可以复制日期', () => {
        const a = new Date()
        a.xxx = { yyy: { zzz: 1} }
        const a2 = deepClone(a)
       assert(a.getTime() === a2.getTime())
        assert(a !== a2)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx !== a2.xxx)
    })
2.2.7. 自动跳过原型属性

如果我们通过Object.create给对象添加一个属性的时候,这个属性会被添加到它的proto里也就是原型上

上图中我们通过Object.create创建了一个a对象并添加了name属性,但是a本身没有name属性,而是在proto里,对于这种在原型上的属性我们不应该去拷贝(原因:如果原型上的每一层都拷贝的话会造成内存爆掉),那么我们怎么实现只拷贝本身的属性哪?
办法:在for in里加上限制条件

for (let key in source) {
    // 如果key是source对象自身的属性才去进行拷贝
     if (source.hasOwnProperty(key)) {
         // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
         deepObj[key] = deepClone(source[key])
       }
}

单元测试

it('自行跳过原型属性', () => {
        const a = Object.create( { name: 'lifa' })
        a.xxx = { yyy: { zzz: 1} }
        const a2 = deepClone(a)
        //a2上没有name属性
        assert.isFalse('name' in a2)
        assert(a !== a2)
        assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
        assert(a.xxx.yyy !== a2.xxx.yyy)
        assert(a.xxx !== a2.xxx)
    })

2.3.问题

上面的代码的问题:cache每次复制完一个对象的时候都没有被清空,所以下一次深拷贝的时候就会互相影响
解决方法:使用面向对象,每次实例化的时候生成一个新的cache
完整代码

class DeepCloner {
    constructor () {
        this.cache = []
    }
    clone(source) {
        if (source instanceof Object) {
            let cacheDist = this.findCache(source)
            if (cacheDist) {
                return cacheDist
            } else {
                let deepObj
                if (source instanceof Array) {
                    deepObj = new Array()
                } else if (source instanceof Function) {
                    deepObj = function() {
                        return source.apply(this, arguments)
                    }
                } else if (source instanceof RegExp) {
                    deepObj = new RegExp(source.source, source.flags)
                } else if (source instanceof Date) {
                    deepObj = new Date(source)
                } else {
                    deepObj = new Object()
                }
                this.cache.push([source, deepObj])
                for (let key in source) {
                    if (source.hasOwnProperty(key)) {
                        // 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
                        deepObj[key] = this.clone(source[key])
                    }
                }
                return deepObj
            }
        }
        return source
    }
    findCache(source) {
        for (let i = 0; i < this.cache.length; i++) {
            if (this.cache[i][0] === source) {
                return this.cache[i][1]
            }
        }
        return undefined
    }
}

module.exports =  DeepCloner
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容