深拷贝与浅拷贝

237 阅读4分钟

js数据类型分为基本数据类型和引用数据类型,基本类型数据保存在在栈内存中,引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。

内存.png

如图所示,基本数据类型的值直接放在栈内存中,而引用数据类型,是将一个变量名作为指针,指向堆内存中的数据。

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。

浅拷贝

举个栗子,浅拷贝:

let obj = {
    a: 1,
    b: 2,
}

let obj1 = obj
console.log(obj1.a)  // 1

obj1.a = 3
console.log(obj.a)   // 3

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

常见的浅拷贝方法:

  • Object.assign
  • Array.prototype.slice()
  • Array.prototype.concat()

示例代码

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

深拷贝

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

常见的深拷贝方式:

  • _.cloneDeep()
  • JSON.stringify() / JSON.parse()
  • 递归循环
  • Object.create()
cloneDeep()

是lodash方法,不做过多解释了,可以去lodash中文官网查看:Lodash 中文网 (lodashjs.com)

JSON.stringify()

方法的原理就是,将引用类型转换成字符串,再通过 JSON.parse() 转换回来,但是这种方法有个问题,就是当数据内部有其他引用类型数据时,会导致将类型变量名转换成字符串,而不是将深层次的引用类型数据一同转换成字符串,所以此方法只适合对象或数据内部只包含有基本数据类型的引用类型数据。

var obj={ 
    name:'xm', 
    birth:new Date, 
    desc:null, 
    ss:[1,2,3], 
    fn:function(){ console.log('123') } 
}
递归

递归则解决了JSON方法的缺点,递归循环的关键在于,要识别每个内部数据的类型,如果是基本数据类型,则简单复制,如果是引用数据类型,则进入下一轮递归。代码如下:

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

补充下,其实对于深拷贝,对象有object.assign方法,数组有Array.concatArray.slice方法,也可以深拷贝,但是和 JSON 一样,都只能拷贝到第一层,内部参数也是一个应用类型的话,则就不行了。

structuredclone

当前浏览器支持一种全局的方法,使用结构化克隆算法将给定的值进行深拷贝。

语法:

let obj = structuredClone(value)
// value:被克隆的对象。可以是任何结构化克隆支持的类型。

let obj = structuredClone(value, { transfer })
// transfer: 是一个可转移对象的数组,里面的 值 并没有被克隆,而是被转移到被拷贝对象上。

structuredclone 所不能做的

  • Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。

  • 对象的某些特定参数也不会被保留

    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。

对比三种深拷贝方法

  • JSON.parse(JSON.stringify(obj)) 方法使用简单,且兼容性好,但是只能处理单层简单的数据类型的深拷贝
  • _.copyDeep 兼容性好,功能强大,对于 Function 类型不会报错,但是缺点是需要注意处理 tree-shaking 否则会有性能损耗
  • structuredClone 方法是浏览器原生支持的,对于 Function 类型会报错,但最主要的缺点是兼容性问题:

全文完