由于js中复制引用类型不会重新拷贝其值,而是复制一个引用地址,共享同一份数据,虽然节省了内存空间,但也存在一些问题,如下。
const obj = {a: 1, b: 2};
// 此时copy的值为{a: 1, b: 2}
const copy = obj;
obj.a = 22;
// 此时obj和copy的值都为{a: 22, b: 2},因为他们共享同一份数据
console.log(obj, copy);
复制代码
因此,有时我们希望拷贝引用类型数据时,是重新创建一份新的数据,而不是拷贝一个引用地址。目前主要有两种方案:1.JSON.parse(JSON.stringify(data));2.使用递归的方式实现。
使用JSON.parse(JSON.stringify())方式实现深拷贝的优缺点
优点
方便快捷、简单明了。
缺点
- 只能拷贝四种值,字符串、数值(十进制)、布尔值和null(NaN, Infinity, -Infinity和undefined都会被转为null);
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)使用此方法,会抛出错误。
使用递归实现深拷贝方式的优缺点
优点
- 支持所有的js数据类型;
- 支持包含循环引用的对象(对象之间相互引用,形成无限循环);
- 可扩展性和可选择性高。
缺点
代码实现较为复杂,晦涩难懂。
由浅入深实现深拷贝
1.先实现一个简单的深拷贝。
代码如下:
const deepClone = (data) => {
// 简单数据类型直接返回值
if (!(data instanceof Object)) {
return data;
}
const newObj = Array.isArray(data) ? [] : {};
for (const key in data) {
// 简单数据类型直接复制值
if (!(data[key] instanceof Object)) {
newObj[key] = data[key];
continue;
}
// 复杂数据类型递归
newObj[key] = deepClone(data[key]);
}
return newObj;
};
const obj = {
a: 1,
b: 2
};
const newObj = deepClone(obj);
obj.a = 10;
console.log(obj, newObj);
复制代码
以上代码实现了一个简单的深拷贝,但也存在一些严重的问题。如果传入一个包含循环引用的对象,则会造成栈溢出。 循环引用:对象之间相互引用,形成无限循环。如下所示:
const obj = {a: 1};
const copy = {b: 2};
// obj对象的新属性b引用copy对象
obj.b = copy;
// copy对象的新属性a引用obj对象
copy.a = obj;
复制代码
2.解决循环引用的问题
我们可以用一个变量保存拷贝过的复杂数据类型的引用地址,然后在每次拷贝复杂数据类型之前,先判断是否拷贝过了。代码如下:
const deepClone = (obj) => {
// 用来保存引用关系,解决循环引用问题(使用闭包私有化hasObj变量)
// 可以weakMap弱引用来保存,这里为了兼容使用object
const hasObj = {};
const clone = (data) => {
// 简单数据类型直接返回值
if (!(data instanceof Object)) {
return data;
}
const newObj = Array.isArray(data) ? [] : {};
for (const key in data) {
// 简单数据类型直接复制值
if (!(data[key] instanceof Object)) {
newObj[key] = data[key];
continue;
}
// 判断是否拷贝过该对象,解决循环引用的问题
if (hasObj[key] === data[key]) {
newObj[key] = data[key];
continue;
}
// 保存引用
hasObj[key] = data[key];
// 复杂数据类型 递归
newObj[key] = clone(data[key]);
}
return newObj;
}
return clone(obj);
};
复制代码
以上代码,虽然解决了循环引用的问题,但是还有一些问题,例如:不能拷贝函数、不能拷贝date对象、不能拷贝正则对象等问题。
3.解决不能拷贝特殊数据类型的问题
我们可以对特殊数据类型做单独处理。完整代码如下:
/**
* @description 深拷贝实现(支持Map、Set、RegExp、Date、Function类型,支持循环引用)
* @param {object} obj 需要深拷贝的对象
* @returns 返回一个新对象
*/
const deepClone = (obj) => {
// 用来保存引用关系,解决循环引用问题(使用闭包私有化copyObj变量)
// 可以weakMap弱引用来保存,这里为了兼容使用object
const copyObj = {};
const clone = (data) => {
// 简单数据类型直接返回值
if (!(data instanceof Object)) {
return data;
}
const newObj = Array.isArray(data) ? [] : {};
for (const key in data) {
// 跳过原型上的属性(可以不跳过)
if (!data.hasOwnProperty(key)) {
continue;
}
// 简单数据类型直接返回值
if (!(data[key] instanceof Object)) {
newObj[key] = data[key];
continue;
}
// 拷贝date对象
if (data[key] instanceof Date) {
newObj[key] = new Date(data[key].getTime());
continue;
}
// 拷贝正则对象
if (data[key] instanceof RegExp) {
newObj[key] = new RegExp(data[key]);
continue;
}
// 拷贝函数
if (data[key] instanceof Function) {
newObj[key] = new Function(`return ${data[key].toString()}`)();
continue;
}
// 拷贝map
if (data[key] instanceof Map) {
newObj[key] = new Map();
data[key].forEach((val, mapKey) => {
if (!(mapKey instanceof Object) && !(val instanceof Object)) {
newObj[key].set(mapKey, val);
} else {
newObj[key].set(clone(mapKey), clone(val));
}
});
continue;
}
// 拷贝set
if (data[key] instanceof Set) {
newObj[key] = new Set();
data[key].forEach((val) => {
if (!(val instanceof Object)) {
newObj[key].add(val);
} else {
newObj[key].add(clone(val));
}
});
continue;
}
// 判断是否为循环引用
if (copyObj[key] === data[key]) {
newObj[key] = data[key];
continue;
}
// 保存引用
copyObj[key] = data[key];
// 复杂数据类型,递归处理
newObj[key] = clone(data[key]);
}
return newObj;
};
return clone(obj);
};
复制代码
以上代码还是有一些问题,不能复制symbol属性和其他的特殊数据类型(如:bigInt),这些问题就留给读者自己解决了。