99%的前端都不知道的深拷贝高阶细节

241 阅读5分钟

主任谈话

有一天我的领导-主任找我谈话,小盾啊觉得自己的js基础水平怎么样。

我实事求是的回答-还阔以。

主任:那你手写一下深拷贝。

退后.jfif

我:内心大喜,我可是看过深拷贝王者版的男人,这不是小菜一碟,忍住心中的窃喜。

淡淡道:没问题。

简简单单的实现

var completeDeepCopy = (data, hash = new WeakMap()) => {
  try {
    if (data == null) return data
    if (data instanceof Date) return new Date(data)
    if (data instanceof RegExp) return new RegExp(data)
    if (typeof data !== 'Object') {
      return data
    }
    if (hash.get(data)) {
      return hash.get(data)
    }
    const copy = new data.constructor()
    hash.set(data, copy)
    for (let key in data) {
      if (data.hasOwnProperty(key)) {
        copy[key] = deepCopy(data[key], hash)
      }
    }
  }
  catch(e) {
    console.log(e)
  }
}

我把这段代码交给了主任

狗头.png

就在我以为自己就要升值加薪-迎娶白富美走上人生巅峰的时候。

主任微微一笑

邪魅一笑.jpg

我立马意识到了事情的严重性。

小盾啊,看的出来你确实意识到了循环引用的问题,也用到了WeakMap。

但是并不是所有的数据都存在循环引用,如果有不存在循环引用的数据,你也进行了set创建,依然有部分的性能消耗。

你有没有办法能够判断是不是存在循环引用,从而避免不必要的weakMap的创建

大吃一惊.png

但是我思来想去-也没想到如何判断数据中是否存在循环引用,无奈只能求助主任大佬

主任出手 随手丢给我一段代码

ar a = {

  b: [1, 2, 3, 4, 5],

  a: { name: 213, tiankong: "456" }

}

a.a  = a

 

try {

  JSON.stringify(a)

} catch(e) {

  console.log(e)

}

主任解释道: JSON.stringify里面转化的数据是不允许有循环引用的,所以可以通过这个来进行判断。

改造完成

我马不停蹄的进行了改造

var completeDeepCopy = (data, hash = new WeakMap()) => {
  try {
    if (data == null) return data
    if (data instanceof Date) return new Date(data)
    if (data instanceof RegExp) return new RegExp(data)
    if (typeof data !== 'Object') {
      return data
    }
    if (hash.get(data)) {
      return hash.get(data)
    }
    const copy = new data.constructor()
    hash.set(data, copy)
    for (let key in data) {
      if (data.hasOwnProperty(key)) {
        copy[key] = deepCopy(data[key], hash)
      }
    }
  }
  catch(e) {
    console.log(e)
  }
}

var easyCopy = (data, hash = new WeakMap()) => {
  try {
    if (data == null) return data
    if (data instanceof Date) return new Date(data)
    if (data instanceof RegExp) return new RegExp(data)
    if (typeof data !== 'Object') {
      return data
    }
    const copy = new data.constructor()
    for (let key in data) {
      if (data.hasOwnProperty(key)) {
        copy[key] = deepCopy(data[key], hash)
      }
    }
  }
  catch(e) {
    console.log(e)
  }
}

var deepCopy = (data) => {
  try {
    JSON.stringify(data)
    return easyCopy(data)
  } catch(e) {
    return completeDeepCopy(data)
  }
}


var a = {
  b: [1, 2, 3, 4, 5],
  a: { name: 213, title: "天才少年" }
}
a.a  = a

var b = deepCopy(a)
console.log(b)

虽然改造完成了,但我们还是要进行一些细节知识的梳理。 有不少小伙伴可能存在疑惑。

循环递归赋值为什么可以深拷贝

这个就涉及到我们今天的核心了深拷贝的核心是什么

核心代码

    const copy = new data.constructor()
    for (let key in data) {
      if (data.hasOwnProperty(key)) {
        copy[key] = deepCopy(data[key], hash)
      }
    }

js的数据类型

js基本数据类型一共有六种:五种简单数据类型和一种复杂数据类型: 五种简单数据类型包括:String、Number、Boolean、undefined、Null 一种复杂数据类型:obeject

js有8种数据类型 六种基本数据类型+symbol+bigInt ES6 中新增了一种 Symbol 。这种类型的对象永不相等,即始创建的时候传入相同的值,可以解决属性名冲突的问题,做为标记。 谷歌67版本中还出现了一种 bigInt。是指安全存储、操作大整数。(但是很多人不把这个做为一个类型)。

引用类型有 Array Object function

基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问

引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针(保存的是堆内存中的引用地址),这个指针指向堆内存。

引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象

面试的时候你就简单记忆就好了 ,五种简单数据类型存的是值,引用类型存的是内存地址(会指向它本身)

看完之后你是不是就会恍然大悟,为什么JSON.parse(JSON.stringify(arr)) 可以深拷贝 或者有些时候slice(),concat,Object.assign有些时候可以实现深拷贝有些时候是浅拷贝 因为它只能剥开一层的值 如果对象里面还有对象或者数组里面还有对象那就无法剥开

递归拷贝你可以理解把数组 或者对象完全剥开,因为递归拷贝会反复取遍历里面的内容。 完全遍历那么里面拿到的值 一定不是引用类型或者object 最后一定就是基本数据类型 那自然而然拿到的值就是深拷贝。

所以深拷贝的原理就是用到了数据的基本类型。

特殊情况特殊处理

当然总有些情况需要进行特殊处理

核心代码

 if (data == null) return data
    if (data instanceof Date) return new Date(data)
    if (data instanceof RegExp) return new RegExp(data)

为什么 new可以实现深拷贝 因为new创建了一个新的实例,开辟了一块新的内存,和原先数据的内存割裂开来,因此也可以实现深拷贝。

为什么使用 weakMap 而不去使用 Map

是因为内存性能开销问题 weakMap持有的只是每个键值对的"弱引用",不会额外开内存保存键值引用。这意味着在没有其他引用存在时,垃圾回收器能正确处理key指向的内存块

喜欢的小伙伴们麻烦点个赞,转发,加收藏,一键三连,虎年大吉。祝你虎虎生威。