精学手撕系列——深浅拷贝原理

 一.JS中浅拷贝的手段有哪些?

10多年的八公山网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。全网营销推广的优势是能够根据用户设备显示端的尺寸不同,自动调整八公山建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联公司从事“八公山网站设计”,“八公山网站推广”以来,每个客户项目都认真落实执行。

1.什么是拷贝?
我们来看下面一个例子,帮助大家区分赋值与拷贝的区别:

 
 
 
  1. let arr = [1, 2, 3]; 
  2. let newArr = arr; 
  3. newArr[0] = 100; 
  4.  
  5. console.log(arr); // [100, 2, 3] 

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

现在进行浅拷贝:

 
 
 
  1. let arr = [1, 2, 3]; 
  2. let newArr = arr.slice(); 
  3. newArr[0] = 100; 
  4.  
  5. console.log(arr); //[1, 2, 3] 

当我们修改newArr的时候,arr的值并不改变,这是因为newArr是arr浅拷贝后的结果,newArr和arr现在是两个不同的引用地址了。

但我们再来看一个潜在的问题:

 
 
 
  1. let arr = [1, 2, {val: 4}]; 
  2. let newArr = arr.slice(); 
  3. newArr[2].val = 1000; 
  4.  
  5. console.log(arr);//[ 1, 2, { val: 1000 } ] 

这里我们明显看到,浅拷贝虽然只能复制一层内容。但如果复制的第一层内容中,有复杂数据类型(数组/对象),那么浅拷贝将失效,这也是浅拷贝最大的限制所在了。

但幸运的是,深拷贝就是为了解决这个问题而生的,它能解决无限层级的对象嵌套问题,实现彻底的拷贝。深拷贝我们在下一道题中介绍。

接下来,我们来归纳一下JS中实现的浅拷贝都有哪些方法呢?

2.手动实现

 
 
 
  1. const shallClone = (target) => { 
  2.   if (typeof target === 'object' && target !== null) { 
  3.     const cloneTarget = Array.isArray(target) ? [] : {}; 
  4.     for (let prop in target) { 
  5.       if (target.hasOwnProperty(prop)) { // 遍历对象自身可枚举属性(不考虑继承属性和原型对象) 
  6.         cloneTarget[prop] = target[prop]; 
  7.     } 
  8.     return cloneTarget; 
  9.   } else { 
  10.     return target; 
  11.   } 

3.Object.assign
但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

 
 
 
  1. let obj = { name: 'sy', age: 18 }; 
  2. const obj2 = Object.assign({}, obj, {Newname: 'sss'}); 
  3. console.log(obj2);  // {name: "sy", age: 18, Newname: "sss"} 

4.concat()浅拷贝数组

 
 
 
  1. let arr = [1, 2, 3]; 
  2. let newArr = arr.concat(); 
  3. newArr[1] = 100; 
  4. console.log(arr); //[ 1, 2, 3 ] 

5.slice()浅拷贝
开头例子就是!!

6. ...展开运算符

 
 
 
  1. let arr = [1, 2, 3]; 
  2. let newArr = [...arr]; //跟arr.slice()是一样的效果 

二.JS中深拷贝的手段有哪些?
在实现一个完整版的深拷贝函数之前,看看有没有某个api能帮助我们完成深拷贝?

1.api版-简易版

 
 
 
  1. JSON.parse(JSON.stringify()); 

从下面例子中,我们可以看出简易版的深拷贝,已经做到了。

 
 
 
  1. let arr = [10, [100, 200], { x: 10, y:20}]; 
  2. let newArr = JSON.parse(JSON.stringify(arr));  
  3. console.log(newArr[2] === arr[2]; //  false 

其实,上面的api,它所使用的是暴力法,什么是暴力法呢,在这里给大家解释一下:

暴力法:把原始数据直接变为字符串,再把字符串变为对象,(此时浏览器会重新开辟所有的内存空间),实现深拷贝。

但是直接供我们使用的api,往往会有一些自己的弊端,比如我们看下面这个例子

 
 
 
  1. let obj = { 
  2.   a: 100, 
  3.   b: [10, 20, 30], 
  4.   c: { 
  5.     x: 10 
  6.   }, 
  7.   d: /^\d+$/, 
  8.    // d: function() {} 
  9.   // d: new Date() 
  10.   // d: BigInt('10') 
  11.   // d: Symbol('f') 
  12. }; 
  13.  
  14. let newObj = JSON.parse(JSON.stringify(obj)); 
  15. console.log(newObj); 
  16. /* 
  17.   {a: 100, b: Array(3), c: {…}, d: {…}} 
  18.     a: 100 
  19.     b: (3) [10, 20, 30] 
  20.     c: {x: 10} 
  21.     d: {} 
  22.     __proto__: Object 
  23.   } 
  24. */ 

从上面例子的输出结果中,我们可以看出,正则属性直接变成了空对象。

那假如我们再把最后一个属性,换成其它类型试一试,我们同理也可以发现,JSON.parse(JSON.stringify()) 都是有弊端的。我们总结一下:

正则属性会变为空对象
函数会直接消失
日期直接字符串
Symbol直接消失
BigInt('10'),直接报错
undefined会直接消失
所以当对象中没有以上形式的属性时,可以用JSON.parse(JSON.stringify())。

但是此方法还有一个弊端,那就是循环引用问题,举个例子:

 
 
 
  1. const a = {value: 2}, 
  2. a.target = a; 

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

2.实现简易版深拷贝
发现上面弊端后,我们来手写一版深拷贝

此种方法,不考虑循环引用问题,也不考虑特殊对象的问题

 
 
 
  1. function deepClone(target)  { 
  2.   if (target === null) return null; 
  3.   if (typeof target !== 'object') return target; 
  4.  
  5.   const cloneTarget = Array.isArray(target) ? [] : {}; 
  6.   for (let prop in target) { 
  7.     if (target.hasOwnProperty(prop)) { 
  8.       cloneTarget[prop] = deepClone(target[prop]); 
  9.     } 
  10.   } 
  11.   return cloneTarget; 

现在我们以发现的几个问题为导向,依次完善深拷贝函数

3.解决循环引用
问题如下:

 
 
 
  1. let obj = { value: 100 }; 
  2. obj.target = obj; 
  3.  
  4. deepClone(obj); //报错: RangeError: Maximum call stack size exceeded 

这就是循环引用。我们怎么来解决这个问题呢?

创建一个Map(Map类似于对象,也是键值对的集合,但是“键”可以是对象),记录下已经拷贝过的对象,如果已经拷贝过,那直接返回就行了.

其原理是每次拷贝引用类型的时候,都设置一个true作为标记,等下次再遍历该对象的时候,就知道它是否已经拷贝过。

 
 
 
  1. const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; 
  2.  
  3. function deepClone (target, map = new Map()) { 
  4.   // 先判断该引用类型是否被 拷贝过 
  5.   if (map.get(target)) { 
  6.     return target; 
  7.   } 
  8.  
  9.   if (isObject(target)) { 
  10.     map.set(target, true); 
  11.     const cloneTarget = Array.isArray(target) ? [] : {}; 
  12.     for (let prop in target) { 
  13.       if (target.hasOwnProperty(props)) { 
  14.         cloneTarget[prop] = deepClone(target[props], map); 
  15.       } 
  16.     } 
  17.     return cloneTarget; 
  18.   } else { 
  19.     return target; 
  20.   } 

现在我们就可以到已经成功了:

 
 
 
  1. const a = {val:2}; 
  2. a.target = a; 
  3. let newA = deepClone(a); 
  4. console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } } 

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:

在计算机程序设计中,弱引用与强引用相对, 是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。--百度百科

说的有一点绕,我用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系,在程序结束之前,a所占的内存空间一直不会被释放,便会造成严重的内存泄漏问题。

怎么解决这个问题呢?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。

稍微改造一下极课:

 
 
 
  1. const deepClone = (target, map = new WeakMap()) => { 
  2.   //... 

4.解决特殊对象问题(RegExp,Date...)
如果传入的对象格式满足,正则或日期格式的话,返回一个新的正则或日期对象的实例

 
 
 
  1. function deepClone (target, map = new Map()) { 
  2.   
  3.   // 检测当前对象target是否与 正则、日期格式对象匹配 
  4.   if (/^(RegExp|Date)$/i.test(target.constructor.name)){ 
  5.     new constructor(target); 
  6.   } 

5.完整版深克隆实现源码

 
 
 
  1. const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; 
  2.  
  3. function deepClone (target, map = new Map()) { 
  4.   // 先判断该引用类型是否被 拷贝过 
  5.   if (map.get(target)) { 
  6.     return target; 
  7.   } 
  8.  
  9.   // 检测当前对象target是否与 正则、日期格式对象匹配 
  10.   if (/^(RegExp|Date)$/i.test(target.constructor.name)){ 
  11.     new constructor(target); 
  12.   } 
  13.  
  14.   if (isObject(target)) { 
  15.     map.set(target, true); 
  16.     const cloneTarget = Array.isArray(target) ? [] : {}; 
  17.     for (let prop in target) { 
  18.       if (target.hasOwnProperty(props)) { 
  19.         cloneTarget[prop] = deepClone(target[props], map); 
  20.       } 
  21.     } 
  22.     return cloneTarget; 
  23.   } else { 
  24.     return target; 
  25.   } 

补充:Object.keys(obj)只遍历私有属性(原型上可能有公共的方法,无法遍历)

当前标题:精学手撕系列——深浅拷贝原理
文章链接:http://www.shufengxianlan.com/qtweb/news32/298232.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联