深拷贝的两种实现方式

3,744 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象

深拷贝则不同,对于复杂引用数据类型,将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,这两个对象是相互独立、不受影响的,彻底实现了内存上的分离

实现深拷贝,一般两种方式方式:

  • 使用JSON.parse(JSON.stringify());
  • 手动实现

JSON.parse(JSON.stringify())版本

JSON.stringify() 是最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象

const obj = {
  name: 'nordon',
  info: {
    age: 18
  }
};

// 一行代码实现深拷贝
const copyObj = JSON.parse(JSON.stringify(obj));

虽然使用JSON.parse(JSON.stringify())实现深拷贝非常简单、便捷,但是其却存在一些API层面的特点:

  • 拷贝的对象的值中如果有函数、undefined、symbol 这三种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失
    • undefined、任意的函数以及 symbol 作为对象属性值时 JSON.stringify() 跳过(忽略)对它们进行序列化
  • 转换值如果有 toJSON() 函数,该函数返回什么值,序列化结果就是什么值,并且忽略其他属性的值
  • 拷贝 Date 引用类型会变成字符串
  • 无法拷贝不可枚举的属性
  • 无法拷贝对象的原型链
  • 拷贝 RegExp 引用类型会变成空对象
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null
  • 无法拷贝包含循环引用的对象(对象之间相互引用,形成无限循环),会抛出错误

手动实现

由于JSON.parse(JSON.stringify())实现深拷贝存在一定的不足,在业务开发中建议慎重使用,因此我们需要自己手动实现一版深拷贝

手动实现过程中需要解决的问题,也就是补齐JSON.parse(JSON.stringify())不足的一个过程,上述已经阐述其不足,接下来直接上代码:

细节在代码中已注释

// 判断是不是引用类型
const isComplexDataType = (obj) => {
	return (typeof obj === "object" || typeof obj === "function") && obj !== null
}

const deepClone = function (obj, hash = new WeakMap()) {
	// 日期对象直接返回一个新的日期对象
	if (obj.constructor === Date) return new Date(obj);
	
	//正则对象直接返回一个新的正则对象
	if (obj.constructor === RegExp) return new RegExp(obj); 

	//如果循环引用了就用 weakMap 来解决
	if (hash.has(obj)) return hash.get(obj);

	let allDesc = Object.getOwnPropertyDescriptors(obj);
	//遍历传入参数所有键的特性
	let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);

	//继承原型链
	hash.set(obj, cloneObj);

	for (let key of Reflect.ownKeys(obj)) {
		cloneObj[key] =
			isComplexDataType(obj[key]) && typeof obj[key] !== "function"
				? deepClone(obj[key], hash)
				: obj[key];
	}
	return cloneObj;
};