深拷贝与浅拷贝

未完待续

深/浅拷贝是容易被问到的一个知识点,考察的是对基础理论的掌握是否扎实。

什么是深拷贝?什么是浅拷贝?

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。举个例子,一个人名叫张三,后来用他拷贝出(假设法律允许)另外一个人叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人,这就是深拷贝了。比较典型的深拷贝就是JavaScript的“值类型”(7种数据类型),如 stringnumberbigintbooleannullundefinedsymbol

深拷贝之外的拷贝叫浅拷贝。

出于节省内存的考虑,JavaScript对“引用类型”(也即第8种数据类型)Object的拷贝默认是浅拷贝。

几种简单的深拷贝

1.JSON内置的方法

(() => {
    let a = { x: 1 }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b)//>> {x:1}
    b.x = 2
    console.log(b)//>> {x:2}
    console.log(a)//>> {x:1}
})();

该方法是用JSON.stringify将对象序列化为字符串,然后在用JSON.parse将json字符串解析为对象,解析的时候会自己去构建新的内存地址存放新对象。

缺点:

  • 会忽略 undefined

  • 会忽略symbol

  • 如果对象的属性为Function,因为JSON格式字符串不支持Function,在序列化的时候会自动删除;

  • 诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失;

  • 不支持循环引用对象的拷贝。

2.Object的内置方法assign

该方法是用Object.assign对对象进行拼接, 将后续对象的内容插入到第一个参数指定的对象,不会修改第一个参数之后的对象,而我们将第一个对象指定为一个匿名空对象,实现深拷贝。

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。

引自MDN。所以这种方法拷贝会有缺陷。

缺点:

  • 对象嵌套层次超过2层,就会出现浅拷贝的状况;

  • 非可枚举的属性无法被拷贝。

3.使用MessageChannel

缺点:

  • 这个方法是异步的;

  • 拷贝有函数的对象时,还是会报错。

壹.3.1.3 递归版深拷贝

前面的深拷贝都有缺点,有没有一种比较理想的办法?有的!

示例代码如下:

壹.3.1.4 循环引用之深拷贝

例如这种情况,obj引用自身:

此时如果调用上面的deepCopy函数的话,会陷入一个死循环,从而导致堆栈溢出。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:

壹.3.1.5 非循环引用的子对象之拷贝

上面已经解决了深拷贝循环引用的问题,但是还不是特别的完善。

现在我们把一个对象想像成一棵树: 对象A有B,C,D三个子对象,其中子对象D中有个属性E引用了对象A下面的子对象B。

用代码来表示就是这样:

此时 A.D.E 与 A.B 是相等的,因为他们引用了同一个对象:

如果再调用刚才的DeepCopy函数深拷贝一份对象A的副本X:

虽然 X.B 和 X.D.E在字面意义上是相等的,但二者并不是引用的同一个对象,这点上来看对象 X和对象A还是有差异的。

这种情况是因为 A.B 并不在 A.D.E 的父级对象链上,所以deepCopy函数就无法检测到A.D.E对A.B也是一种引用关系,所以deepCopy函数就将A.B深拷贝的结果赋值给了 X.D.E。

知道原因那么解决方案就呼之欲出了:父级的引用是一种引用,非父级的引用也是一种引用,那么只要记录下对象A中的所有对象,并与新创建的对象一一对应即可。

测试一下看看:

壹.3.1.6 终极完美方案:lodash

lodash_.cloneDeep()支持循环对象、大量的内置类型,对很多细节都处理的比较不错,推荐使用。

那么,lodash是如何解决循环引用这个问题的?查阅源码:

会发现lodash是用一个栈记录了所有被拷贝的引用值,如果再次碰到同样的引用值的时候,不会再去拷贝一遍,而是利用之前已经拷贝好的。

综上,在生产环境,我们推荐使用lodash的cloneDeep()

数组的浅拷贝: 如果是数组,我们可以利用数组的一些方法,比如 slice,concat 方法返回一个新数组的 特性来实现拷贝,但假如数组嵌套了对象或者数组的话,使用 concat 方法克隆并不完整, 如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或数组,就会只拷 贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化,我们 把这种复制引用的拷贝方法称为浅拷贝,

深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也互相分离,修改一个对象 的属性,不会影响另一个

如何深拷贝一个数组

1、这里介绍一个技巧,不仅适用于数组还适用于对象!那就是:

原理是 JSON 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可 以把 JSON 字符串反序列化为一个 js 对象,通过这两个方法,也可以实现对象的深复制。 但是这个方法不能够拷贝函数 浅拷贝的实现: 以上三个方法 concat,slice ,JSON.stringify 都是技巧类,根据实际项目情况选择使用,我 们可以思考下如何实现一个对象或数组的浅拷贝,遍历对象,然后把属性和属性值都放 在一个新的对象里即可

var shallowCopy = function(obj) { // 只拷贝对象 if (typeof obj !== 'object') return;

// 根据 obj 的类型判断是新建一个数组还是对象 var newObj = obj instanceof Array ? [] : {}; // 遍历 obj,并且判断是 obj 的属性才拷贝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; 深拷贝的实现 那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型, 如果是对象,我们递归调用深拷贝函数不就好了~ var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; }

参考链接

https://coffe1891.gitbook.io/frontend-hard-mode-interview/1/1.3.1

最后更新于