不积跬步之手写深拷贝

深拷贝与浅拷贝.png

浅拷贝和深拷贝的区别

大家都以为浅拷贝就是把引用类型的值拷贝一份,实际还是引用的同一个对象,把这个叫浅拷贝,实际上这是一个大大的误会。

  • 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后相互影响的问题。
  • 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址。源对象和拷贝对象还是会相互影响
  • 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和和拷贝对象互相影响。

实现浅拷贝的方式有哪些呢?

  1. Object.assign
  2. 数组的slice和concat方法
  3. 数组静态方法Array.from
  4. 扩展运算符

实现深拷贝

要求:

  • 支持对象、数组、日期、正则的拷贝。
  • 处理Map 和 Set
  • 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
  • 处理 Symbol 作为键名的情况。
  • 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,没有任何问题)。
  • 处理 DOM 元素(DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个)。
  • 额外开辟一个储存空间 WeakMap,解决循环引用递归爆栈问题(引入 WeakMap 的另一个意义,配合垃圾回收机制,防止内存泄漏)。

我们要实现的目标:

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;

1.最简单的实现,不考虑引用类型

要拷贝的对象

const target = {
      a: true,
      b: 100,
      c: 'str',
      d: undefined,
};

要拷贝这个对象,我们只需要把对象里面的数据按个拷贝出来就可以了。

function deepClone(target){
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

2.判断处理 Null的情况

const target = {
      a: true,
      b: 100,
      c: 'str',
      d: undefined,
      e: null,
};

我们只需要添加一个判断就可以了。

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

3.判断处理日期的情况

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
};

日期是一个对象,我们可以通过日期的构造函数重新new一个对象来进行复制

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

4.判断处理正则的情况

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
};

正则和日期一样,同样也可以使用构造函数来处理。

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

5.深拷贝复制引用类型

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    }
};

里面既然有了引用类型,那么我们只需要递归调用一下,然后返回就可以了。
arguments.callee 这里指向函数的引用。

function deepClone(target){
    
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    
    //处理引用类型 以免死循环
    if(typeof target !== 'object'){
        return target;
    }
   
    const cloneTarget = {} 
    for(const key in target){
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

6.处理数组的情况

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4]
};

数组这了比较简单,它的区别仅仅只是我们拷贝的是对象还是数组。

function deepClone(target){
    
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //处理引用类型 以免死循环
    if(typeof target !== 'object'){
        return target;
    }
    // 处理对象和数组 以及原型链
    const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
    for(const key in target){
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

可以看到上面有一个骚操作,就是我们通过实例的constructor拿到它的构造函数,然后直接new 就可以了。
这样就不用在去判断是否是数组还是对象,然后去调用它对应的构造函数,当然这里这样写有一定的风险。如果作为底层库来使用,需要考虑constructor并没有指向它构造函数的情况。

不过通过它的构造函数我们解决了另外一个问题,就是原型链。通过它原本的构造函数,原型链自然也是完整保存的。意想不到的小细节

7.处理Symbol的情况

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    [Symbol("name")]:"job",
};

Symbol的特性就是全局唯一值,里面的参数只是一个描述符。而并不是具体值。这里在处理的时候,我们需要考虑两种情况。

  1. 一种是作为值存在的Symbol
  2. 第二种是作为键存在的Symbol

作为值存在的话,我们可以通过Symbol.prototype.toString方法拿到它的描述字段。然后重新构造一个Symbol

//处理值为Symbol的情况
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }

作为键存在的话,for in的遍历范围就无法满足我们的要求的,所以需要换成遍历范围更加合适的Reflect.ownKeys()

下面是MDN的原话

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。
它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。

同时使用Reflect.ownKeys解决了for in的一个隐藏的问题。那就是会把原型对象的属性也遍历下来,然后存储到拷贝对象里面。

升级后的效果:


function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    //处理日期
    if(target instanceof Date){
        return new Date(target);
    }
    //处理正则表达式
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //处理值为Symbol的情况
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }
    //处理引用类型 以免死循环
    if(typeof target !== 'object'){
        return target;
    }
    // 处理对象和数组
    const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
    //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key]);
    })
    return cloneTarget;
}

8.处理循环引用的情况

const target = {
    a: true,
    b: 100,
    ...
};

target.target = target;

数据是上面的这样。这种的数据会让递归进入死循环从而造成爆栈的问题。
这里可以通过WeakMap来建立当前对象和拷贝对象的弱引用关系,判断当前对象是否存在,如果存在就使用它
保存的对象,如果不存在就添加进去。

WeakMap的原理是,它的键值只能是引用类型,它的这个引用类型并不会强制标记,当垃圾回收机制需要释放内存的时候,它会被直接释放,而不需要做其他的操作,使用者也不担心内存泄漏的问题。

我们用WeakMap改造升级一下。

function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    //处理日期
    if(target instanceof Date){
        return new Date(target);
    }
    //处理正则表达式
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //处理值为Symbol的情况
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }
    //处理引用类型 以免死循环
    if(typeof target !== 'object'){
        return target;
    }
    //判断释放存在
    if(map.has(target)){
        return target;
    }
    // 处理对象和数组
    const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
    //保存原引用和拷贝引用的关系
    map.set(target,cloneTarget)
    //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key],map);
    })
    return cloneTarget;
}

这样就可以处理循环引用的问题了。

9.处理 Set和Map的情况 和HTMLElement的情况

因为这两个都是可迭代的数据结构,同时它们又有自己的添加属性的方法。所以需要按个判断。

由于之前 const cloneTarget = new target.constructor(),我们并不需要去手动添加处理MapSet

而HTMLElement的情况并不需要处理,拷贝也没有意义,直接返回就好。

function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null) return target;
    //处理日期
    if(target instanceof Date)  return new Date(target);
    //处理正则表达式
    if(target instanceof RegExp)  return new RegExp(target);
    //处理值为Symbol的情况
    if(typeof target === 'symbol') return Symbol(target.toString());
    // 处理 DOM元素
    if (target instanceof HTMLElement) return target
    //非引用类型的直接返回 比如函数 就不需要处理,直接返回就好
    if(typeof target !== 'object') return target;
    //从缓冲中读取
    if(map.has(target)) return target;
    // 处理对象和数组
    const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
    //保存原引用和拷贝引用的关系
    map.set(target,cloneTarget)
    //处理Map的情况
    if(target instanceof Map){
       for(let [key,value] of target){
           target.set(key,deepClone(value,map));
       }
       return target;
    }
    //处理set的情况
    if(target instanceof Set){
        for(let value of target){
            target.add(deepClone(value,map))
        }
        return target;
    }
    //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key],map);
    })
    return cloneTarget;
}

差不多就是这样了。我们处理了下面的情况:

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;

打印结果:

{
  a: true,
  b: 100,
  c: 'str',
  d: undefined,
  e: null,
  f: 2022-03-30T09:47:13.762Z,
  g: /abc/,
  h: { a: 'ccc', b: 12 },
  i: [ 1, 2, 3, 4 ],
  j: Symbol(Symbol(age)),
  k: Set(4) { 1, 2, 3, 4 },
  l: Map(2) { 1 => 2, 3 => 4 },
  target: <ref *1> {
    a: true,
    b: 100,
    //这里省略折起...
  },
  [Symbol(name)]: 'job'
}

如果真的要实现一个深拷贝 ,那么情况要复杂的多,可以参考lodash的源码学习。

聊一下另外一个深拷贝的方式: JSON.parse(JSON.stringify(target))

我们使用这个来实现深拷贝 ,直接深拷贝上面的测试用例,看看能够有几个?

const target = {
    0:NaN,
    1:Infinity,
    2:-Infinity,
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;
JSON.parse(JSON.stringify(target))
//报错:
VM8398:1 Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'target' closes the circle
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:17

意思循环引用错误。我们把循环引用的代码去掉:

target.target = target;

看一下打印出来的结果:

'0': null,
'1': null,
'2': null,
a: true
b: 100
c: "str"
e: null
f: "2022-03-30T11:20:08.362Z"
g: {}
h: {a: 'ccc', b: 12}
i: (4) [1, 2, 3, 4]
k: {}
l: {}
[[Prototype]]: Object

从头看到尾:

  1. d: undefined,没有了,无法处理值 为undefined的情况
  2. f:2022-03-30T11:20:08.362Z时间变成了字符串
  3. g: {} 正则表达式 没有了
  4. j:Symbol("age") 所有Symbol的值都没有了,
  5. k:new Set([1,2,3,4]) Set 没有了
  6. l:new Map([[1,2],[3,4]]) Map 没有了

会忽略的有 : undefined,Symbol,函数 ,直接不存在
会变成对象:Map,Set,正则表达式
会被序列化为Null:NaNInfinity-Infinity

不能循环引用。

学习参考的文章:感谢这些大佬,我才能站在巨人的肩膀上。
轻松拿下 JS 浅拷贝、深拷贝
如何写出一个惊艳面试官的深拷贝?

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

推荐阅读更多精彩内容