几种前端深拷贝的用法与优缺点?

1,166 阅读6分钟

背景:

很多前端小伙伴都习惯使用JSON解构赋值进行深拷贝,但其实几种方式都有对应的优缺点,估计小伙伴都不一定知道这些优缺点,特别是一些刚入行没多久的开发,所以才想写个文章细说各种拷贝方式的优缺点,让我们在合适的场景选择合适的方式,如果只是想看深拷贝方式或者他们的坑可以直接划到中间观看。

一、前置知识点:

先要理解什么是引用数据类型,什么是基本数据类型才会知道为什么会有深浅拷贝。

(1)基本数据类型

JavaScript基本数据类型是最简单也是最常用的数据类型,基本数据类型是直接存储在栈内存中的,当我们将一个基本类型的值赋给另一个变量的时候,实际上是复制了这个值,修改一个变量时并不会影响到其他变量。

其中JavaScript基本数据类型包括以下几种:

  • Number: 其中包括整数与浮点数。
  • String: 字符串类型。
  • Boolean: 布尔值,有 truefalse 两个值。
  • Undefined: 表示还未定义的值。
  • Null: 表示空值,其实是一个特殊的 undefined 类型。
  • Symbol: 在ES6中扩展引入的原始数据类型,意思表示独一无二的值。
  • BigInt: 在ES10中扩展引入的原始数据类型,意思表示任意精度的大整数。

(2)引用数据类型

JavaScript引用数据类型是存储在堆内存中的,变量中保存的是指向该堆内存中对象的引用地址(或者叫指针),当我们将一个引用类型的值赋给另一个变量的时候,实际上是复制了这个引用地址(或者叫指针)。当我们修改一个引用类型的变量会影响到其他引用相同对象的变量,因为他们是使用同一个引用地址。

其中JavaScript引用数据类型主要包括以下几种:

  • Object: 包括了普通对象、数组和函数等。
  • Array: 数组其实就是对象,是对象的一种特殊形式。
  • Function: 函数也是对象,是对象的一种特殊形式。
  • Date: 日期对象。
  • RegExp: 正则表达式对象。
  • Error: 错误对象。

二、什么是深拷贝什么是浅拷贝?:

理解了基本的数据类型和引用数据类型之后,下面就说说什么是深拷贝,什么是浅拷贝。

深拷贝:拷贝的时候把对象从内存中完完整整地拷贝一份出来,然后在堆中开辟一片新的区域来存放新的对象。大白话说就是:两个对象互相独立互不影响。

浅拷贝:浅拷贝只会复制对象的第一层属性(最表层)。对于基本数据类型它会把值复制;但对于引用类型属性,它只会复制这个引用地址(或者叫指针),一旦修改内容会把新旧属性一起被修改,因为它们使用同一个引用地址。

举例说明:

var obj={
  name:'初始值',
  age:18
};

var newobj = obj;
newobj.name = '天天鸭';

console.log(newobj);//打印出:{name:'天天鸭',age:18}
console.log(obj);//打印出:{name:'天天鸭',age:18}

上面是针对浅拷贝例子,可以看出来,在引用数据类型中直接使用=蜀国符其实是浅拷贝,赋值的是地址,所以改变newobj时,obj也会被改变。

因此我们要开一个新的空间存放newobj,那么newobjobj就会相互独立。所以以下举例深拷贝例子。

var obj={
   name:'初始值',
   age:18
};

var newobj = JSON.parse(JSON.stringify(obj));
newobj.name = '天天鸭';

console.log(newobj);//打印出:{name:'天天鸭',age:18}
console.log(obj);//打印出:{name:'初始值',age:18}

如上代码所示,使用了深拷贝后两个对象就会互不影响,深拷贝在实际开发中要经常用到。因此下面总结了几种常用的深拷贝方式,并且说明了每种的缺点防止在项目中出现异常还不知道原因。

三、总结几种深拷贝方式与它们的坑

(1) JSON.stringify() 与 JSON.parse()

let obj = {
   a:NaN,
   b:undefined,
   c:new RegExp(/\d/),
   d:new Date(),
   d:Infinity,
   e:new Error(),
}
let newobj = JSON.parse(JSON.stringify(obj))

如上代码所示,JSON.stringify() 配合 JSON.parse() 这算是最常用的深拷贝方法了,但这种方式其实有不少坑。例如深拷贝后下面这些特殊的值会发生对应的变化,这会导致一样莫名其妙的BUG

缺陷:
(1)undefined ==> 空

(2)NaN ==> null

(3)时间戳 ==> 字符串时间

(4)Infinity ==> null

(5)错误信息 ==> 空对象

(6)无法实现对象中方法(fountion)的深拷贝

(2)Object.assign()

用法:

let obj = {name:'天天鸭',age:25}; 

let obj2 = {...obj} 

var obj3 = Object.assign({},obj);

优点:数量较少而且浅层的时候使用方便。

缺点:只能复制对象的第一层属性(表层),对于嵌套的对象或数组,它只会复制引用而不是创建新的副本。因此不支持特殊类型的拷贝,例如 Map、Set、WeakMap、WeakSet 等。 如下所示:

const obj = {
  a: 3,
  b: {
      c: 4
  },
  d: [5, 6, 7]
};

const newobj = Object.assign({}, obj);

newobj.a = 18  //  由于第一层,深拷贝生效
newobj.b.c = 3;  //  深拷贝不生效
newobj.d[0] = 6;  //  深拷贝不生效

(3)...解构赋值

let obj = { age: 18, name: '天天鸭' } 

let newobj = {...obj}; 

newobj.age = 22; 

console.log(obj.age); // 18

和上面Object.assign情况有点类似,如果只是一层数组或是对象,其元素只是简单类型的元素,那么属于深拷贝(就是只能一层拷贝,暂时就理解为深拷贝吧!!!!)

(4) 手动封装递归深拷贝

function deepClone(sou) {
  if (!isObject(sou)) return sou; // 如果不是对象就直接返回

  // 如果传入的是数组
  if (Array.isArray(sou)) {
      return sou.map(item => deepClone(item));
  }

  // 如果传入的是对象
  const _obj = {};
  for (const key in sou) {
      if (sou.hasOwnProperty(key)) { // 确保只复制自身的属性
          _obj[key] = deepClone(sou[key]);
      }
  }

  // 处理特殊对象类型
  if (sou instanceof Date) {
      return new Date(sou.getTime());
  } else if (sou instanceof RegExp) {
      return new RegExp(sou);
  } else if (typeof sou === 'function') {
      return function() {
          return sou.apply(this, arguments);
      };
  }

  return _obj;
}

function isObject(obj) {
  return typeof obj === 'object' && obj !== null;
}

小结:

除了上面这些深拷贝方式还有不少库直接封装好的,能直接引入使用例如:lodash

但如果我们使用上面几种传统的方法就要注意它们的优缺点了,毕竟都是不完善的。好啦到这种就写完啦,如果哪里写的不对或者有更好的建议欢迎大佬指出哈。