返回

深拷贝,不只是JSON.parse和JSON.stringify

前端

这年头,“深拷贝”和“浅拷贝”可谓是程序员面试的必考题。许多人自诩对这俩概念已经烂熟于心,一聊起原理来头头是道,一套一套的。但只要一问到具体方法,就总是那句老生常谈:“用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));
}