之前我们介绍了 JavaScript 的两种数据类型,分别是基础数据类型和引用数据类型,今天这篇文章讲的是深拷贝和浅拷贝,实际上就是围绕着这两种数据类型展开的
在 JavaScript 编程中,我们经常需要对数据进行复制,那什么时候使用深拷贝,什么时候使用浅拷贝呢,这值得我们思考
浅拷贝 是对一个对象的顶层结构的复制,它会创建一个新的对象,但是这个新对象只复制了原对象的堆内存引用,而不是对象本身。浅拷贝会导致新对象和原对象共享同一个堆内存。也就是说,如果原对象的属性值发生改变,新对象的属性值也会跟着改变,反之亦然。
object.assign是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
缺点
从上面的代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
利用 JavaScript 的扩展运算符,在构造对象的同时完成浅拷贝的功能
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
slice 方法也比较有局限性,因为它仅仅针对数组类型。slice方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
从上面的代码中可以看出,这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下:
将一个对象从内存中完整的拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离
JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象
缺点
虽然他还是有以上的各种问题,但足以满足日常的开发需求,简单快捷
使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的,那么就需要下面的几种方法了
下面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify 一样,还是有一些问题没有完全解决,例如:
这种基础版本的写法也比较简单,可以应对大部分的应用情况。但还是有不少的缺陷,那怎么办呢?、
不用担心,我们还有进阶版本
针对上面几个待解决问题,我们应该怎么做呢?
针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys(可以获取一个对象的所有属性,包括可枚举属性、不可枚举属性和 Symbol 类型属性) 方法 当参数为 Date、Regexp类型,则直接生成一个新的实例返回 利用 Object.getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create 方法创建一个新对象,并继承传入原对象的原型链 利用 weakMap 类型作为 Hash 表,因为 weakMap 是弱引用类型,可以有效防止内存泄露,作为检测循环引用很有帮助,如果存在循环,则引用直接返回 weakmap 存储的值
实现深拷贝
在本篇文章中,我们分别介绍了深浅拷贝以及各自的优缺点,之后由浅入深,一步一步的讲解手写深浅拷贝方法的思路和实现,这对我们深入了解js底层原理有很大的帮助
作者:每天都有好果汁吃
链接:https://juejin.cn/post/7264208575974834231