深拷贝,不只是JSON.parse和JSON.stringify
2023-11-01 09:37:38
这年头,“深拷贝”和“浅拷贝”可谓是程序员面试的必考题。许多人自诩对这俩概念已经烂熟于心,一聊起原理来头头是道,一套一套的。但只要一问到具体方法,就总是那句老生常谈:“用JSON.parse和JSON.stringify啊!”有函数的话就递归循环拷贝。
可是,如果对象里嵌套了函数呢?递归或者循环拷贝该怎么处理?
更何况,JSON.parse和JSON.stringify有其自身的局限性。比如,它不能拷贝函数、日期、正则表达式、错误对象等类型的数据。
所以,今天这篇文章,我们就来详细聊聊深拷贝和浅拷贝的那些事儿。
深拷贝与浅拷贝的原理和区别
原理
深拷贝是指将一个对象的所有属性,包括其子对象,都复制一份新的对象。浅拷贝则是只复制对象本身的属性,而不会复制其子对象。
区别
举个例子,我们有一个对象obj,包含两个属性:一个字符串属性name和一个数组属性friends。
const obj = {
name: '张三',
friends: ['李四', '王五']
};
如果我们对obj进行浅拷贝,那么新对象newObj将拥有与obj相同的name和friends属性,但friends属性指向的仍然是obj中的那个数组。
const newObj = Object.assign({}, obj);
也就是说,如果我们修改newObj中的friends属性,那么obj中的friends属性也会随之改变。
newObj.friends.push('赵六');
console.log(obj); // { name: '张三', friends: [ '李四', '王五', '赵六' ] }
而如果我们对obj进行深拷贝,那么newObj将拥有与obj完全相同的name和friends属性,但friends属性指向的是一个新的数组。
const newObj = JSON.parse(JSON.stringify(obj));
也就是说,如果我们修改newObj中的friends属性,那么obj中的friends属性不会受到影响。
newObj.friends.push('赵六');
console.log(obj); // { name: '张三', friends: [ '李四', '王五' ] }
深拷贝和浅拷贝的实现方法
JSON.parse和JSON.stringify
前面已经提到,JSON.parse和JSON.stringify可以实现对象的深拷贝。但需要注意的是,这种方法只适用于那些可以被JSON序列化的对象。
比如,函数、日期、正则表达式、错误对象等类型的数据就不能被JSON序列化,因此也不能通过JSON.parse和JSON.stringify进行深拷贝。
递归
递归是一种常见的深拷贝方法。基本思路是,先检查对象是否包含子对象,如果有,则递归地对子对象进行深拷贝,然后将子对象的深拷贝结果赋值给父对象的相应属性。
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepCopy(item));
}
const newObj = {};
for (const key in obj) {
newObj[key] = deepCopy(obj[key]);
}
return newObj;
}
循环
循环也是一种常见的深拷贝方法。基本思路是,使用循环逐层遍历对象,并为每个属性创建一个新的副本。
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepCopy(item));
}
const newObj = {};
for (const key in obj) {
newObj[key] = deepCopy(obj[key]);
}
return newObj;
}
其他方法
除了JSON.parse/stringify、递归和循环之外,还有许多其他方法可以实现深拷贝,比如:
- Object.assign() :Object.assign()方法可以将一个或多个源对象的属性复制到目标对象。但需要注意的是,Object.assign()只能进行浅拷贝。
- lodash.cloneDeep() :lodash.cloneDeep()方法可以实现对象的深拷贝。它比JSON.parse/stringify和递归更加高效。
- fast-deep-equal :fast-deep-equal库提供了一个高效的深拷贝方法。它可以处理循环引用和函数等复杂数据结构。
针对不同类型数据的深拷贝解决方案
前面提到了JSON.parse/stringify、递归和循环这三种深拷贝方法。但需要注意的是,这三种方法并不适用于所有的数据类型。
比如,函数、日期、正则表达式、错误对象等类型的数据就不能被JSON.parse/stringify序列化,因此也不能通过JSON.parse/stringify进行深拷贝。
而递归和循环虽然可以实现对象的深拷贝,但对于某些特殊的数据类型,比如函数、日期、正则表达式、错误对象等,它们也无法正确地进行深拷贝。
因此,我们需要针对不同的数据类型,采用不同的深拷贝解决方案。
对象
对于对象,我们可以使用递归或循环进行深拷贝。
数组
对于数组,我们可以使用map方法进行深拷贝。
const newObj = obj.map(item => deepCopy(item));
函数
对于函数,我们可以使用Function.prototype.toString()方法获取函数的字符串表示,然后使用eval()方法重新创建一个函数。
const newObj = eval('(' + obj.toString() + ')');
日期
对于日期,我们可以使用new Date()方法创建一个新的日期对象。
const newObj = new Date(obj.getTime());
正则表达式
对于正则表达式,我们可以使用new RegExp()方法创建一个新的正则表达式对象。
const newObj = new RegExp(obj.source, obj.flags);
错误对象
对于错误对象,我们可以使用Error()方法创建一个新的错误对象。
const newObj = new Error(obj.message);
DOM元素
对于DOM元素,我们可以使用cloneNode()方法创建一个新的DOM元素。
const newObj = obj.cloneNode(true);
文件对象
对于文件对象,我们可以使用FileReader()方法读取文件,然后使用Blob()方法创建一个新的文件对象。
const reader = new FileReader();
reader.onload = function() {
const newObj = new Blob([reader.result]);
};
reader.readAsArrayBuffer(obj);
Blob对象
对于Blob对象,我们可以使用Blob()方法创建一个新的Blob对象。
const newObj = new Blob([obj]);
ArrayBuffer对象
对于ArrayBuffer对象,我们可以使用ArrayBuffer.slice()方法创建一个新的ArrayBuffer对象。
const newObj = obj.slice(0);
FormData对象
对于FormData对象,我们可以使用FormData()方法创建一个新的FormData对象。
const newObj = new FormData();
for (const key of obj.keys()) {
newObj.append(key, obj.get(key));
}
Map对象
对于Map对象,我们可以使用new Map()方法创建一个新的Map对象。
const newObj = new Map();
for (const [key, value] of obj.entries()) {
newObj.set(key, deepCopy(value));
}
Set对象
对于Set对象,我们可以使用new Set()方法创建一个新的Set对象。
const newObj = new Set();
for (const value of obj) {
newObj.add(deepCopy(value));
}