深拷贝 与 浅拷贝

136 阅读4分钟

不同数据类型的存储:栈与堆

  • 基本数据类型:Number、String 、Boolean、Null、Undefined、Symbol 和 Bigint。值存在里。
  • 引用数据类型:Object。包括Object、Function、Array、Data、RegExp 类。值存在里,里存的是指针,指向存储堆里的对象的地址。

注意:const 定义的常量,是引用数据类型时,栈内存的是指针指向的地址,由指针指向堆内存中,里面有真正的数据,不可改变的是指针,真正的数据是可以更改的。因此,const的常量不能再修改指针,但是可以更改数值。

const obj = {}
const obj1 = {"name":"lucy"}
obj1["name"] = "tom"  //成功
obj1 = obj2           //报错

这就是一次浅拷贝,对 p2 属性的修改会导致 p1 属性也被修改:

const p1 = {name: 'Tom'};
const p2 = p1;
p2.name = 'lucy';
console.log(p1.name);  // "lucy"

 是一种 先进后出 的数据结构,当声明一个基本数据时,它就会被存储到栈内存中。

const a = 1;
const b = "1";

复制:对应内存的数据复制一份到新开辟的内存中:

const c = b;

在JS的内存世界里: image.png 栈内存的地址分配是连续的,b、c两个变量占用了不同的存储空间,所以他们之间也并没有什么联系

特点:存取速度快,但不灵活,同时由于结构简单,在变量使用完成后就可以将其释放,内存回收容易实现。

引用类型(对象)的引用地址存在栈中,数据存在堆里,但是堆内存存储变量时没有什么规律可言。

const p1 = {"name":"tom"};
const p2 = p1;

image.png

复制后,p2和p1都是指向同一个地址,这就说明对 p1 进行修改时就会影响到 p2 的值,这时还是浅拷贝

简言之,深拷贝就是,把对象 a 拷贝成 b 后,a 与 b 之间不再有交集,对 对象 b 的修改不会修改到 a。

实现深拷贝

方法一:用 JSON

const b = JSON.parse(JSON.stringify(a))

JSON.stringify将对象变成字符串,JSON.parse再将字符串变成对象。

该方法有缺陷:

  • 不支持Date、正则、undefined、函数等数据
  • 不支持引用(环状结构)

方法二:递归

const deepClone = (a, cache) => { 
  if(!cache){ 
    cache = new Map() // 缓存 
  } 
  if(a instanceof Object) { // 对象
    if(cache.get(a)) { 
      return cache.get(a) 
    } 
    let result = undefined
    if(a instanceof Function) { 
      if(a.prototype) { // 普通函数 
        result = function(){ return a.apply(this, arguments) } 
      }else { // 箭头函数
        result = (...args) => { return a.call(undefined, ...args) } 
      } 
    } else if(a instanceof Array) { // 数组
        result = [] 
    } else if(a instanceof Date) { // 日期
        result = new Date(a - 0) 
    } else if(a instanceof RegExp) { // 正则
        result = new RegExp(a.source, a.flags) 
    } else { 
        result = {} 
    } 
    cache.set(a, result) 
    for(let key in a) { 
      if(a.hasOwnProperty(key)){ 
        result[key] = deepClone(a[key], cache) 
      } 
    } 
    return result 
  } else { //基本类型
    return a 
  } 
} 

验证一下

const a = { 
  number:1,
  bool:false,
  str: 'hi',
  empty1: undefined,
  empty2: null,
  array: [ {name: 'frank', age: 18}, {name: 'jacky', age: 19} ],
  date: new Date(2000,0,1,20,30,0),
  regex: /\.(j|t)sx/i,
  obj: { name:'frank', age: 18}, 
  f1: (a, b) => a + b,
  f2: function(a, b) { return a + b } 
} 
a.self = a 
const b = deepClone(a) 
b.self === b // true 
b.self = 'hi' 
a.self !== 'hi' // true
b.self === a.self // false

image.png

image.png 按照传入的数据类型,来返回不同的内容:

  • 引用:1.首先,构造要返回的本体 result,初始值 undefined。2.然后再按照对象的不同类来确定返回的类型
    • 函数:普通/箭头,返回 function(){ return a.apply(this, arguments) / (...args) => { return a.call(undefined, ...args) }
    • 数组:返回 []
    • 日期:返回 new Date(a - 0)
    • 正则:返回 new RegExp(a.source, a.flags)
    • 其他:返回 {} 3.遍历将对象 a 的所有属性都递归地拷贝到 b 里面
  • 基本:直接返回 a

注意

  • 判断 a 是否是对象/函数/数组/日期/正则,用 a instanceof Object/Function/Array/Date/RegExp ,判断函数是普通/箭头函数,用a.prototype
  • a.self = a 使 a 中出现了环,此时递归地遍历 a 的属性去创建 b 是有问题的,需要用 Map 去记录是否访问过,如果访问过就直接返回访问过的这个属性值,(map 的key可以是对象,Object的key只能是String/Symbol)
  • 不拷贝原型上的属性:有些属性是继承得到的,不应该对它深拷贝,用a.hasOwnProperty(key) 去筛选

总结

深拷贝的主要思想就是:递归地判断数据是引用/基本类型,遍历要进行深拷贝的对象的属性,不拷贝原型上的属性。