前言
之前我们介绍了 JavaScript 的两种数据类型,分别是基础数据类型和引用数据类型,今天这篇文章讲的是深拷贝和浅拷贝,实际上就是围绕着这两种数据类型展开的
在 JavaScript 编程中,我们经常需要对数据进行复制,那什么时候使用深拷贝,什么时候使用浅拷贝呢,这值得我们思考
浅拷贝
浅拷贝是对一个对象的顶层结构的复制,它会创建一个新的对象,但是这个新对象只复制了原对象的堆内存引用,而不是对象本身。浅拷贝会导致新对象和原对象共享同一个堆内存。也就是说,如果原对象的属性值发生改变,新对象的属性值也会跟着改变,反之亦然。
object.assign
object.assign
是 ES6 中object
的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝
。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。object.assign 的语法为:Object.assign(target, ...sources)
缺点
- 不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举的属性
ini
let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);
从上面的代码中可以看到,利用 object.assign
也可以拷贝 Symbol
类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题
,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
扩展运算符
利用 JavaScript 的扩展运算符,在构造对象的同时完成浅拷贝的功能
ini
扩展运算符的语法为:`let cloneObj = { ...obj };`
css
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和 object.assign
有同样的缺陷,也就是实现的浅拷贝的功能差不多
,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
concat 拷贝数组
数组的
concat
方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过concat
只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
ini
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
slice拷贝数组
slice
方法也比较有局限性,因为它仅仅针对数组类型
。slice方法会返回一个新的数组对象
,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
arduino
slice 的语法为:arr.slice(begin, end);
ini
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
从上面的代码中可以看出,这就是浅拷贝的限制所在了------它只能拷贝一层对象
。如果存在对象的嵌套,那么浅拷贝将无能为力
。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝
思路
- 对基础类型做一个最基本的拷贝
- 对引用数据类型开辟一个新的存储,并且拷贝一层对象属性(并且还是和原对象指向同一个内存空间)
实现
javascript
const shallowClone = (target) => {
// 判断目标对象是否为对象类型且不为null
if (typeof target === 'object' && target !== null) {
// 创建一个克隆对象 cloneTarget,如果目标对象是数组则创建一个空数组,否则创建一个空对象
const cloneTarget = Array.isArray(target) ? []: {};
// 遍历目标对象的属性,使用 for...in 循环,判断属性是否为对象本身的属性(而非原型链上的属性)
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
// 给克隆对象的属性赋值,将目标对象的属性值赋给克隆对象的属性
cloneTarget[prop] = target[prop];
}
}
// 返回克隆对象
return cloneTarget;
} else {
// 如果目标对象不是对象类型或为null,则直接返回目标对象
return target;
}
}
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
深拷贝原理及实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝
。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下
:
将一个对象从内存中完整的拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离
乞丐版 JSON.stringify()
JSON.stringify()
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON
的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse()
的方法将 JSON
字符串生成一个新的对象
css
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
缺点
- 拷贝的对象的值如果有
函数
,undefined
,symbol
这几种类型,经过 JSON.stringify() 序列后的字符串中这个键值对会消失 - 拷贝
Date引用类型
会变成字符串 - 无法拷贝不可枚举的属性
- 无法拷贝对象的原型链
- 拷贝
RegExp引用类型
会变成空对象 - 对象中含有
NaN
,Infinity
以及-Infinity
,JSON.parse()序列化的结果会变成null
- 无法拷贝对象的循环引用,即
对象成环(obj[key]=obj)
的情况
ini
function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
虽然他还是有以上的各种问题,但足以满足日常的开发需求,简单快捷
使用 JSON.stringify
方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify
暂时还是无法满足的,那么就需要下面的几种方法了
基础版(手写递归实现)
下面是一个实现 deepClone
函数封装的例子,通过 for in
遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
scss
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify
一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及
Symbol
类型; - 这种方法
只是针对普通的引用类型的值做递归复制
,而对于Array、Date、RegExp、Error、Function
这样的引用类型并不能正确地拷贝; - 对象的属性里面成环,即
循环引用没有解决
。
这种基础版本的写法也比较简单,可以应对大部分的应用情况。但还是有不少的缺陷,那怎么办呢?、
不用担心,我们还有进阶版本
进阶版(递归实现)
针对上面几个待解决问题,我们应该怎么做呢?
- 针对能够遍历对象的不可枚举属性以及
Symbol
类型,我们可以使用Reflect.ownKeys(可以获取一个对象的所有属性,包括可枚举属性、不可枚举属性和 Symbol 类型属性)
方法- 当参数为
Date、Regexp
类型,则直接生成一个新的实例返回- 利用
Object.getOwnPropertyDescriptors
方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create
方法创建一个新对象,并继承传入原对象的原型链- 利用
weakMap
类型作为Hash
表,因为weakMap
是弱引用类型,可以有效防止内存泄露,作为检测循环引用很有帮助,如果存在循环,则引用直接返回weakmap
存储的值
实现深拷贝
javascript
// 判断一个对象是否为复杂数据类型,即对象或函数类型,且不为 null
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
// 定义深拷贝函数 deepClone,接受两个参数:obj 为要进行深拷贝的目标对象,hash 为已经拷贝过的对象的哈希表(用于解决循环引用问题)
const deepClone = function (obj, hash = new WeakMap()) {
// 如果目标对象是日期对象,则直接返回一个新的日期对象,避免修改原日期对象
if (obj.constructor === Date) {
return new Date(obj)
}
// 如果目标对象是正则对象,则直接返回一个新的正则对象,避免修改原正则对象
if (obj.constructor === RegExp){
return new RegExp(obj)
}
// 如果目标对象已经被拷贝过,则从 hash 中获取已经拷贝过的对象并返回,避免出现循环引用问题
if (hash.has(obj)) {
return hash.get(obj)
}
// 获取目标对象的所有属性描述符
let allDesc = Object.getOwnPropertyDescriptors(obj)
// 创建一个新对象 cloneObj,并将其原型链指向 obj 的原型对象
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 将目标对象和克隆对象的映射关系存入 hash 中,用于解决循环引用问题
hash.set(obj, cloneObj)
// 遍历目标对象的所有属性(包括字符串类型和 Symbol 类型的属性名)
for (let key of Reflect.ownKeys(obj)) {
// 如果目标对象的属性值是复杂数据类型(即对象或数组),则递归调用 deepClone 函数进行深拷贝,并将拷贝结果赋值给克隆对象的对应属性
if (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') {
cloneObj[key] = deepClone(obj[key], hash)
} else {
// 如果目标对象的属性值不是复杂数据类型,则直接将其赋值给克隆对象的对应属性
cloneObj[key] = obj[key]
}
}
// 返回深拷贝后的新对象
return cloneObj
}
javascript
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
总结
在本篇文章中,我们分别介绍了深浅拷贝以及各自的优缺点,之后由浅入深,一步一步的讲解手写深浅拷贝方法的思路和实现,这对我们深入了解js底层原理有很大的帮助
最后,祝大家变得更强!