JavaScript进阶——02-浅拷贝和深拷贝

前言

在 JavaScript 的编程中经常需要对数据进行复制,这就涉及到浅拷贝和深拷贝,是非常重要的概念。

浅拷贝

概念

创建一个新的对象B,来接收你要重新复制的对象A的值:

  • 如果对象A里面的属性是基本类型,拷贝的是基本类型的值;

  • 但如果对象A里面的属性是引用类型,拷贝的是内存中的地址(不是拷贝)。也就是说,拷贝后的内容和原始内容,指向的是同一个地址。如果一个对象的属性值发生了变化,另一个对象的属性值也会发生变化。

浅拷贝在拷贝引用类型的数据时,只拷贝第一层的属性,再深层的属性无法进行拷贝。用一个成语形容叫“藕断丝连”。

深拷贝

概念

创建一个新的对象B,来接收你要重新复制的对象A的值:

  • 在堆内存中开辟了一块全新的内存地址,将对象A的属性完全复制过来。

  • 这两个对象相互独立、互不影响,彻底实现了内存上的分离。

下面讲一下实现深拷贝的几种方式。

方式1:JSON.stringify() 和 JSON.parse()

这是最简单的深拷贝方法,先把对象序列化成 json 字符串,然后将JSON 字符串生成一个新的对象。

代码实现:

 let obj1 = { a:1, b:[1,2,3] }
 let str = JSON.stringify(obj1);
 let obj2 = JSON.parse(str);
 console.log(obj2);   //{a:1,b:[1,2,3]}
 ​
 obj1.a = 2;
 obj1.b.push(4);
 console.log(obj1);   //{a:2,b:[1,2,3,4]}
 console.log(obj2);   //{a:1,b:[1,2,3]}

方式1属于乞丐版。缺点是:

(1)主要缺点:

  • 无法拷贝函数、undefined、symbol。经过 JSON.stringify 序列化之后的字符串中这个键值对会消失。

  • 无法拷贝 Map、Set;

  • 无法拷贝对象的循环引用,即 obj[key] = obj。

(2)其他缺点:

  • 拷贝 Date 引用类型会变成字符串;

  • 拷贝 RegExp 引用类型会变成空对象;

  • 无法拷贝不可枚举的属性;

  • 无法拷贝对象的原型链;

  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

无法拷贝函数的代码举例:

 const obj = { fn: () => {}, name: 'qianguyihao' };
 console.log(JSON.stringify(obj)); // {"name":"qianguyihao"}

无法拷贝循环引用的代码举例:

 const obj = { fn: () => {}, name: 'qianguyihao' };
 obj.self = obj;
 /*
     控制台报错:
         Uncaught TypeError: Converting circular structure to JSON
         --> starting at object with constructor 'Object'
         --- property 'self' closes the circle
         at JSON.stringify (<anonymous>)
 */
 console.log(JSON.stringify(obj));

小结:如果你的数据结构是简单的数据类型,使用方式1是最简单和快捷的选择;但如果数据类型稍微复杂一点,方式1 就不行了。

方式2:手写递归

如果只考虑简单的数组、对象,方式2是满足要求的。

 const obj1 = {
     name: 'qianguyihao',
     age: 30,
     address: {
         city: 'shenzhen'
     }
 }
 ​
 const obj2 = deepClone(obj1)
 obj2.address.city = 'beijing'
 console.log(obj1.address.city)
 ​
 /**
  * 深拷贝
  * @param {Object} obj 要拷贝的对象
  */
 function deepClone(obj = {}) {
     // 1、判断是值类型还是引用类型
     if (typeof obj !== 'object' || obj == null) {
         // obj 如果不是对象和数组,或者是 null,就直接return
         return obj
     }
 ​
     // 2、判断是数组还是对象
     // 初始化返回结果:数组或者对象
     let result
     if (obj instanceof Array) {
         result = []
     } else {
         result = {}
     }
 ​
     for (let key in obj) {
         // 保证 key 不是原型的属性
         if (obj.hasOwnProperty(key)) {
             // 3、递归【关键代码】
             result[key] = deepClone(obj[key])
         }
     }
 ​
     return result
 }
 ​
 let obj1 = {
   a:{
     b:1
   }
 }
 let obj2 = deepClone(obj1);
 obj1.a.b = 2;
 console.log(obj2);   //  {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 obj1 = {
     a:{
         b:1
     }
 }
 let obj2 = deepClone(obj1);
 obj1.a.b = 2;
 console.log(obj2);   //  {a:{b:1}}

方式2只考虑了 object 和 Array这种 对普通的引用类型的值,是属于比较基础的深拷贝。缺点是:

(1)主要缺点:

  • 无法拷贝函数 Function。

  • 无法拷贝 Map、Set。

  • 无法拷贝对象的循环引用,即 obj[key] = obj。

(2)其他缺点:

  • 无法拷贝不可枚举的属性以及 Symbol 类型。

  • 无法拷贝 Date、RegExp、Error 这样的引用类型。

方式3:改进版

针对上面几个问题,可以用如下几点改进:

(1)针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;

(2)当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;

(3)利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;

(4)利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。

 /**
  * 深拷贝
  * @param obj obj
  * @param map weakmap 为了避免循环引用
  */
 function cloneDeep(obj, map = new WeakMap()) {
     if (typeof obj !== 'object' || obj == null ) return obj
 ​
     // 避免循环引用
     const objFromMap = map.get(obj)
     if (objFromMap) return objFromMap
 ​
     let target = {}
     map.set(obj, target)
 ​
     // Map
     if (obj instanceof Map) {
         target = new Map()
         obj.forEach((v, k) => {
             const v1 = cloneDeep(v, map)
             const k1 = cloneDeep(k, map)
             target.set(k1, v1)
         })
     }
 ​
     // Set
     if (obj instanceof Set) {
         target = new Set()
         obj.forEach(v => {
             const v1 = cloneDeep(v, map)
             target.add(v1)
         })
     }
 ​
     // Array
     if (obj instanceof Array) {
         target = obj.map(item => cloneDeep(item, map))
     }
 ​
     // Object
     for (const key in obj) {
         const val = obj[key]
         const val1 = cloneDeep(val, map)
         target[key] = val1
     }
 ​
     return target
 }

希望各位可以点个赞点个关注,这对up真的很重要,谢谢大家啦!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

被瞧不起的神

谢谢啦,感谢支持|一起努力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值