1、数据类型
说起拷贝,就不得不提起 js 的数据类型了,因为深拷贝和浅拷贝的核心就在于不同的数据类型在内存中存储的地方不同。
JavaScript中存在基本类型和引用类型。其中基本类型数据保存在栈内存中,栈具有先进后出的特点;引用类型数据保存在堆内存中,引用数据类型的变量是存放在栈中的,指向的是堆内存中实际对象的引用。
首先我们要知道最新的 ECMAScript 标准定义了 8 种数据类型,其中 7 种是基本数据类型,它们是String、Number、Boolean、Null、Undefined、Symbol、BigInt。和对象类型Object。
基本类型:
1)字符串(String),字符串是一串表示文本值的字符序列,例如:“不染-何程龙” 。
2)数字(Number),整数或浮点数,例如: 42 或者 3.14159。
3)布尔值(Boolean),有2个值分别是:true 和 false。
4)null , 一个表明 null 值的特殊关键字。 JavaScript 是大小写敏感的,因此 null 与 Null、NULL或变体完全不同。
5)undefined ,和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性。
6)代表(Symbol)( 在 ECMAScript 6 中新添加的类型).。一种实例是唯一且不可改变的数据类型。Symbol 函数栈不能用 new 命令。Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for...in 、for...of的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames()返回。如果要读取到一个对象的Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和Reflect.ownKeys() 取到。
Symbol.for() 类似单例模式,首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值,如果有即返回该 Symbol 值,若没有则新建并返回一个以该字符串参数为名称的 Symbol 值,并登记在全局环境中供搜索。
Symbol.keyFor() 返回一个已登记的 Symbol 类型值的 key ,用来检测该字符串参数作为名称的 Symbol 值是否已被登记。
7)任意精度的整数 (BigInt),可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
对象类型Object:
2、深拷贝与浅拷贝
浅拷贝是指创建新的数据,将源对象的属性拷贝一份。如果属性是基本类型,拷贝的是基本类型的值;如果为引用类型,拷贝的是内存地址。修改对象属性会影响原对象。
常用的浅拷贝方法:
1)展开运算符 ...
2)Object.assign():用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
3)concat和slice数组方法
如果拷贝的对象中属性有引用类型值的话,浅拷贝就不能达到预期的完全复制隔离的效果了。
深拷贝是开辟了一个新的栈,两个对象属性相同。将拷贝过程中遇到的引用类型都新开辟一块地址拷贝对应的数据,对应两个不同的地址,避免子对象共享同一份内存的问题了,修改一个不会影响另一个。
JSON.parse(JSON.stringify()) 将对象先转成字符串,再通过JSON.parse将字符串转成对象,此时对象中每个层级的堆内存都是新开辟的。存在的问题:1)不能解决循环引用的问题;2)无法拷贝特殊对象,比如:RegExp、BigInt、Date、Set、Map等。
3、手写深拷贝
1)JSON.stringify
var copy_data = JSON.parse(JSON.stringify(origin_data))
实现一个功能类似JSON.parse(JSON.stringify())的简单深拷贝,能对对象和数组进行深拷贝:
不能解决循环引用的问题;无法拷贝特殊对象,比如:RegExp、BigInt、Date、Set、Map等
2)递归方法
3)浅拷贝 + 递归
对于基本数据类型,我们直接拷贝即可;对于引用数据类型,则需要进行递归拷贝。
我们使用拷贝对象的构造方法创建对应类型的数据。
首先使用Object.prototype.toString.call()来获取对象的准确类型。
获取到了具体的引用类型后,我们可以根据对应的类型进行初始化对象的操作。通过target.constructor拿到拷贝对象的构造函数,通过源对象的构造函数生成的对象可以保留对象原型上的数据,如果使用{},则原型上的数据会丢失。
1)Boolean、Number、String、Date、Error我们可以直接通过构造函数和原始数据创建一个新的对象。
2)Object、Map、Set我们直接执行构造函数返回初始值,递归处理后续属性,因为它们的属性可以保存对象。
3)Array、Symbol、RegExp进行特殊处理。
整体代码框架:
首先我们对于参数进行判断其是否为对象类型,如果是普通类型,直接返回即可。
这里我们还增加了缓存机制,为了防止自身的递归调用,陷入死循环。我们使用了WeakSet进行储存,因为成员都是弱引用,可以被垃圾回收机制回收,不容易造成内存泄漏;
使用了Object.prototype.toString.call()来获取拷贝对象的准确类型。根据不同的类型利用其的构造方法创建对应类型的数据。如果是map和set,通过独有的set、add方法设置值,单独处理。
首先是创建拷贝对象,我们可以根据对应的类型进行初始化对象的操作。
1)通过target.constructor拿到拷贝对象的构造函数,通过源对象的构造函数生成的对象可以保留对象原型上的数据
2)Boolean、Number、String、Date、Error我们可以直接通过构造函数和原始数据创建一个新的对象。
3)Object、Map、Set我们直接执行构造函数返回初始值,递归处理后续属性,因为它们的属性可以保存对象。
4)Array、Symbol、RegExp进行特殊处理。
对Array、Symbol、RegExp进行特殊处理。
浅拷贝: