知识扫盲
为了照顾一些基础不太好的同学,这里简单举一个小例子,让大家明白为什么要实现对象的深拷贝?
我们编写一段简单的JS代码。
代码如下:
let userA = { name: "张三", age: 24 } let userB = userA; userB.age = 30; console.log(userA);
输出结果:
上段代码中我们简单的将userA赋值给了userB,然后更改了userB的age,但是控制台打印userA的时候发现age也变了,这就是问题所在。
通过上面的列子可以简单的看出,对象不能通过简单的赋值,所以我们衍生出了浅拷贝和深拷贝,上面那段代码可以简单理解为浅拷贝,通常我们浅拷贝很容易出现问题,所以我们一般复制对象的时候都是用深拷贝,本篇文章着重讲深拷贝。
问题归根结底:
两个对象引用了同一个内存地址。
1. 利用JSON
这种实现深拷贝的方式非常的简单,利用的是JSON.parse()和JSON.stringify()两个方法。
原理介绍:
JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,在这转换过程中,会开辟新的内存空间,也就实现了深拷贝。
示例代码:
let userA = { name: "张三", age: 24 } let userB = JSON.parse(JSON.stringify(userA)); userB.age = 30; console.log(userA);
输出结果:
可以看到此时我们的userA没有受到影响,说明两个对象独立开来了。
虽然这种方式非常的简单,但是它并没有那么完善,有一些情况它还是解决不了的,主要问题如下:
- 忽略
undefined
和symbol
- 无法拷贝function
- 无法拷贝循环引用对象(比如对象属性的值就是它自己)
- 对Date、RegExp等一些内置函数的处理会出现问题,比如时间不再是对象格式。
问题拷贝代码:
let userA = { name: "张三", age: 24, num: undefined, sex: () => { console.log("我是函数") }, time: new Date() } let userB = JSON.parse(JSON.stringify(userA)); userB.age = 30; console.log("对象userA", userA); console.log("对象userB", userB);
输出结果:
可以看到我们对象userB的function不见了,时间也变为了字符串格式,num字段也不见了,这就是问题所在。
2. 递归实现
这种方式也是大家比较常用的,也是面试中小伙伴回答得最多的,有些小伙伴可能背得滚瓜烂熟,但是有很大部分小伙伴只知道这个方法,让他手撕代码却写不出来。
递归实现的原理很简单,就是循环遍历我们的对象,如果遇到属性是对象的,再一次递归循环遍历即可。
示例代码:
let userA = { name: "张三", age: 20, sex: () => { console.log("小猪课堂"); }, arr: ["1", "231", "342242"], obj: { like: "打游戏" }, likesFood: new Set(["fish", "banana"]), date: new Date() }; function deepClone(obj) { // 如果传入的类型不对,则不做处理 if (typeof obj !== "object" || obj === null) { return; } let newObj = {}; // 新对象 const dataKeys = Object.keys(obj); // 获取原对象所有键值 dataKeys.forEach((value) => { const currentValue = obj[value]; // 基础类型直接赋值 if (typeof currentValue !== "object" || currentValue === null) { newObj[value] = currentValue; } else if (Array.isArray(currentValue)) { // 实现数组的深拷贝 newObj[value] = [...currentValue]; } else if (currentValue instanceof Set) { // 实现set数据的深拷贝 newObj[value] = new Set([...currentValue]); } else if (currentValue instanceof Map) { // 实现map数据的深拷贝 newObj[value] = new Map([...currentValue]); } else if (currentValue instanceof Date) { // 日期类型深拷贝 newObj[value] = new Date(currentValue.valueOf()) } else { // 普通对象则递归赋值 newObj[value] = deepClone(currentValue); } }); return newObj; } let userB = deepClone(userA); userB.obj.like = "睡觉" console.log("userA",userA) console.log("userB",userB)
输出效果:
上面的代码将我们常见的类型实现了深拷贝,比如function、对象、数组、日期等等,基本上能够满足我们日常的开发,如果还有自己需要深拷贝的类型,大家直接再加判断即可。
问题所在:
虽然递归能够满足我们日常中大部分列子,很多小伙伴用的就是这种方法,但是它也有问题所在,比如循环引用的问题,对象的属性值就是该对象。
代码如下:
let userA = { name: "张三", age: 20, sex: () => { console.log("小猪课堂"); }, arr: ["1", "231", "342242"], obj: { like: "打游戏" }, likesFood: new Set(["fish", "banana"]), date: new Date() }; userA.name = userA;
执行结果:
可以看到我们就加了一行代码,将name的值改为了userA,这就产生了循环引用/此时我们在使用该递归进行深拷贝,直接报栈溢出了,进入了死循环!
3. 递归实现升级版
为了解决循环引用的问题,我们稍微改造一下我们的代码即可。
问题解决思路:
我们需要判断要拷贝的对象,是不是已经拷贝过,而不要循环拷贝,这里我们借助缓存的思想。
实现代码:
let userA = { name: "张三", age: 20, sex: () => { console.log("小猪课堂"); }, arr: ["1", "231", "342242"], obj: { like: "打游戏" }, likesFood: new Set(["fish", "banana"]), date: new Date() }; userA.name = userA; function deepClone(obj, hashMap = new WeakMap()) { // 如果传入的类型不对,则不做处理 if (typeof obj !== "object" || obj === null) { return; } // 查缓存字典中是否已有需要克隆的对象,有的话直接返回同一个对象(同一个引用,不用递归无限创建进而导致栈溢出了) if (hashMap.has(obj)) { return hashMap.get(obj); } let newObj = {}; // 新对象 const dataKeys = Object.keys(obj); // 获取原对象所有键值 dataKeys.forEach((value) => { const currentValue = obj[value]; // 基础类型直接赋值 if (typeof currentValue !== "object" || currentValue === null) { newObj[value] = currentValue; } else if (Array.isArray(currentValue)) { // 实现数组的深拷贝 newObj[value] = [...currentValue]; } else if (currentValue instanceof Set) { // 实现set数据的深拷贝 newObj[value] = new Set([...currentValue]); } else if (currentValue instanceof Map) { // 实现map数据的深拷贝 newObj[value] = new Map([...currentValue]); } else if (currentValue instanceof Date) { // 日期类型深拷贝 newObj[value] = new Date(currentValue.valueOf()) } else { hashMap.set(obj, newObj); // 哈希表缓存新值 // 普通对象则递归赋值 newObj[value] = deepClone(currentValue,hashMap); } }); return newObj; } let userB = deepClone(userA); userB.obj.like = "睡觉" console.log("userA", userA) console.log("userB", userB)
实现效果:
上段代码解决了我们循环引用的问题,主要利用了WeakMap()这个方法,它的作用其实主要就是创建一个缓存表,我们深拷贝时,会先判断该属性值是否在缓存表当中,存在则说明是循环引用。
想入深入了解Weakmap的小伙伴可以参考官网,我们这里只实现方法:MDN
4. Object.assign()
Object.assign()是一种比较简单的深拷贝方法,其实说它是深拷贝,一点都不严谨,因为它只会拷贝第一层,也就是当对象内嵌套有对象的时候,被嵌套的对象进行的还是浅拷贝。
示例代码:
let userA = { name: "张三", age: 24 } let userB = Object.assign({},userA); userB.age = 30; console.log("userA",userA); console.log("userB",userB);
输出结果:
上段代码实现了简单的深拷贝,在一般的开发过程中,我们还是可以使用的,使用起来还是比较简单,详细用法可以参考官网:MDN
问题所在:
无法进行深层次的拷贝,只能进行对象的第一层深拷贝。
5. 利用Lodash库(完美)
这个库相信很多小伙伴都用过,简直是前端开发利器。lodash是一套工具库,内部封装了很多字符串、数组、对象等常见数据类型的处理函数。
它提供了给我们进行深拷贝的方法,各种情况他都考虑到了,如果不在乎引入第三方库的话,强烈建议使用此方法。
示例代码:
_.cloneDeep(obj1);
直接调用提供给我们的_.cloneDeep方法即可。
官方中文文档:官方文档
6. 利用Jquery
Jquery提供了深拷贝的方法给我们,但是目前这个大前端环境下,很多人不在使用jquery,所以这儿就简单介绍一下。
示例代码:
$.extend(true,{},obj) 复制代码
直接调用extend方法即可。
总结
这里只举例了常用的一些方法,还有很多奇奇怪怪的方法这里就不过多说了。但是深拷贝的原理始终都是不变的,我们需要解决的问题就是开辟一块新的内存空间,只要朝着这个思路下去,方法总是有很多。
欢迎关注公众号:资料分享大师(专注分享)
哔哩哔哩:小猪课堂(视频教程)
知乎:会飞的猪(作者动态)