漫话深浅拷贝

78 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

前言

前段时间公司招聘小伙伴,在寻找合适的partners的过程中发现了一个问题:有的小伙伴对于深浅拷贝不是很清楚。在日常开发中,碰到的一些灵异事件就有可能是引用类型拷贝不彻底导致的,所以我这里打算详细的针对深浅考虑来唠一下。

适用范围:对深浅拷贝不熟悉或者有疑问的小伙伴

浅拷贝

什么是浅拷贝?

用大白话来说就是,只对深度为1的属性来进行拷贝,对于属性的值直接进行赋值。用官方话来说就是,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

需要特殊说明的是,浅拷贝不等于引用赋值(个人理解,如有错误,烦请斧正)。在我看来,引用赋值是对整个对象的指针的拷贝,新的对象和原始对象完全相等;而对于浅拷贝来说,创建的是一个全新的对象,新的对象和原始对象不相等,而且只有值为引用数据类型的属性才会出现共用内存的情况。

常见的浅拷贝的方法

  • Object.assign
  • Array.prototype.concat
  • ...(解构赋值)
  • for循环(非递归)

浅拷贝的特殊

由于浅拷贝对于属性的拷贝是直接引用赋值,这就导致使用浅拷贝的两个数据会存在一些互通,进而产生一些灵异事件。尤其是在进行代码调试,且输出内容为引用数据类型时,需要注意处理。

我明明没有修改对象b,对象b为什么发生了变化呢?

    const a = {
        name: 'a',
        features: {
            size: 1,
            color: '#333'
        }
    }
    
    const b = Object.assign({}, a);
    
    a.name = 'newA';
    a.features.size = 2;
    
    // b
    // {
    //    name: 'a',
    //    features: {
    //        size: 2,
    //        color: '#333'
    //    }
    // }

深拷贝

深拷贝又怎么说?

深拷贝就是对原始对象进行一次全方位的复制,而不是局限于深度为1的属性。

在进行赋值之前,为指针类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝 [1]  。这种拷贝称为深拷贝。

常见的深拷贝的方法

  • JSON.stringify + JSON.parse
  • 递归

深拷贝方法的优劣

  1. JSON.stringify + JSON.parse

对于这种方法来说,优势是使用起来简单方便,可以满足大多数的使用常见;但是劣势也很明显,由于JSON.stringify本身的问题,导致含有特殊数据类型的时候,拷贝出来的新对象跟原始对象不一致。比如:undefined、函数、日期等。

当然,我们可以通过重新JSON.stringify的replacer方法来解决,相对于递归拷贝来说,可读性不高。

  1. 递归

递归应该是深拷贝的基本操作了,深拷贝的优势是适用于所有数据类型,不会出现不适用的场景。但是深拷贝也有自身的问题,那就是使用姿势不正确会导致栈溢出。

纳尼?深拷贝还会导致栈溢出?

是的,深度优先的递归理论上是会出现的,产生的原因是,由于闭包导致作用域无法及时释放。当然,解决办法也很简单,大致有两种方法:

  • 广度优先递归
  • 尾递归调用

有关广度优先和深度优先的闲聊我们放到另外一篇文章中进行,这里就不多说了。

结语

好了,看到这里是不是对于深浅拷贝初步有了大致的了解了呢?如果还有其它疑问或者文章中描述不正确的地方,欢迎在下方留言讨论。