编辑
2023-07-23
手写源码系列
00
请注意,本文编写于 369 天前,最后修改于 366 天前,其中某些信息可能已经过时。

目录

什么是深拷贝
为什么需要深拷贝
如何实现深拷贝
如何处理循环引用
总结

深拷贝是一种复制对象的方法,它可以保证复制出来的对象和原对象完全独立,不会相互影响。本文将从底层原理分析深拷贝和浅拷贝的区别,探讨深拷贝的必要性和应用场景,以及用JavaScript语言实现不同情况下的深拷贝方法,包括考虑和不考虑循环引用的情况。

什么是深拷贝

JavaScript中,对象是一种复杂的数据类型,它是由多个属性和方法组成的。当我们需要复制一个对象时,有两种方式:浅拷贝和深拷贝。

浅拷贝是指只复制对象的第一层属性,也就是说,如果对象的属性值是基本类型(如字符串、数字、布尔值等),那么就直接复制这个值;如果对象的属性值是引用类型(如数组、对象、函数等),那么就只复制这个引用,也就是内存地址。

这样,浅拷贝出来的对象和原对象共享同一个引用类型的属性,如果修改其中一个对象的引用类型属性,另一个对象也会受到影响。

深拷贝是指完全复制对象的所有层级属性,也就是说,无论对象的属性值是基本类型还是引用类型,都会重新创建一个新的值,并赋给新对象。这样,深拷贝出来的对象和原对象完全独立,不会相互影响。

为什么需要深拷贝

深拷贝的必要性主要在于保证数据的安全性和一致性。在一些场景中,我们需要对一个对象进行修改,但又不想影响到原对象,或者我们需要保存一个对象的历史状态,以便于后续的比较或回滚。这些情况下,如果使用浅拷贝,就会导致数据的混乱或丢失。而使用深拷贝,则可以避免这些问题。

例如,在开发一个网页游戏时,我们可能需要保存玩家的角色信息,包括姓名、等级、装备、技能等。如果我们想要给玩家提供一个试穿装备的功能,让玩家可以预览不同装备对角色属性的影响,但又不改变玩家当前的装备状态,那么我们就需要对玩家的角色信息进行深拷贝,然后在新对象上进行修改。如果使用浅拷贝,则会导致玩家当前的装备被修改,影响游戏体验。

如何实现深拷贝

实现深拷贝有多种方法,其中最常见和最简单的一种是使用递归。递归是指函数自己调用自己的过程,它可以处理任意层级的嵌套结构。递归实现深拷贝的基本思路是:

  • 判断输入是否是一个对象(包括数组、函数等),如果不是,则直接返回输入;
  • 创建一个新对象(或数组),用于存放复制出来的属性;
  • 遍历输入对象的所有属性(或元素),对每个属性(或元素)进行以下操作:
    • 如果属性值(或元素)是一个对象(或数组),则递归调用自身函数,传入该属性值(或元素),并将返回值赋给新对象(或数组)对应的属性(或元素);
    • 如果属性值(或元素)不是一个对象(或数组),则直接复制该属性值(或元素),并赋给新对象(或数组)对应的属性(或元素);
  • 返回新对象(或数组)。

JavaScript语言实现的递归深拷贝函数如下:

js
function deepClone(obj) { // 判断输入是否是一个对象 if (typeof obj !== 'object' || obj === null) { // 如果不是,则直接返回输入 return obj; } // 创建一个新对象(或数组) let result = Array.isArray(obj) ? [] : {}; // 遍历输入对象的所有属性(或元素) for (let key in obj) { // 如果属性值(或元素)是一个对象(或数组) if (typeof obj[key] === 'object' && obj[key] !== null) { // 则递归调用自身函数,传入该属性值(或元素),并将返回值赋给新对象(或数组)对应的属性(或元素) result[key] = deepClone(obj[key]); } else { // 如果属性值(或元素)不是一个对象(或数组),则直接复制该属性值(或元素),并赋给新对象(或数组)对应的属性(或元素) result[key] = obj[key]; } } // 返回新对象(或数组) return result; }

除了递归,还有其他的实现深拷贝的方法,例如:

  • 使用JSON.stringify()JSON.parse()方法,将对象转换为字符串,再转换为新对象。这种方法简单易用,但是有一些局限性,比如不能处理函数、循环引用、日期、正则表达式等特殊类型的数据;
  • 使用Object.assign()方法,将源对象的所有可枚举属性复制到目标对象。这种方法可以处理一层的深拷贝,但是如果源对象的属性值是引用类型,那么还是会出现浅拷贝的问题;
  • 使用展开运算符(...),将源对象的所有可枚举属性展开,然后赋给目标对象。这种方法和Object.assign()方法类似,也只能处理一层的深拷贝。

如何处理循环引用

循环引用是指一个对象的属性值引用了自身或者自身的某个祖先对象。例如:

js
let a = {}; let b = {}; a.b = b; b.a = a;

这种情况下,如果使用递归深拷贝函数,就会出现无限递归的问题,导致栈溢出错误。为了解决这个问题,我们需要在递归过程中记录已经访问过的对象,并判断当前对象是否已经被访问过。

如果已经被访问过,则直接返回之前复制出来的对象,而不再进行递归。这样就可以避免无限递归的问题。

为了实现这个功能,我们需要使用一个额外的数据结构来存储已经访问过的对象和它们对应的复制出来的对象。

在本文中,我们使用Map来实现,因为Map可以使用任意类型的值作为键,并且有更好的性能。

修改后的考虑循环引用的递归深拷贝函数如下:

javascript
function deepClone(obj, map = new Map()) { // 判断输入是否是一个对象 if (typeof obj !== 'object' || obj === null) { // 如果不是,则直接返回输入 return obj; } // 判断当前对象是否已经被访问过 if (map.has(obj)) { // 如果已经被访问过,则直接返回之前复制出来的对象 return map.get(obj); } // 创建一个新对象(或数组) let result = Array.isArray(obj) ? [] : {}; // 将当前对象和新对象存入Map中 map.set(obj, result); // 遍历输入对象的所有属性(或元素) for (let key in obj) { // 如果属性值(或元素)是一个对象(或数组) if (typeof obj[key] === 'object' && obj[key] !== null) { // 则递归调用自身函数,传入该属性值(或元素)和Map,并将返回值赋给新对象(或数组)对应的属性(或元素) result[key] = deepClone(obj[key], map); } else { // 如果属性值(或元素)不是一个对象(或数组),则直接复制该属性值(或元素),并赋给新对象(或数组)对应的属性(或元素) result[key] = obj[key]; } } // 返回新对象(或数组) return result; }

总结

深拷贝是一种复制对象的方法,它可以保证复制出来的对象和原对象完全独立,不会相互影响。深拷贝和浅拷贝的区别在于是否复制引用类型的属性值。

深拷贝的必要性在于保证数据的安全性和一致性。实现深拷贝有多种方法,其中最常见和最简单的一种是使用递归。

递归实现深拷贝需要注意处理循环引用的问题,可以使用一个额外的数据结构来记录已经访问过的对象。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:CreatorRay

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!