-.- -.-
首页
  • 分类
  • 标签
  • 归档
  • Lodash 源码分析 (opens new window)
Mozilla (opens new window)
GitHub (opens new window)

江月何年初照人

首页
  • 分类
  • 标签
  • 归档
  • Lodash 源码分析 (opens new window)
Mozilla (opens new window)
GitHub (opens new window)
  • 算法

  • JS相关内容

    • 从深拷贝看JS中的循环引用
      • 0.1+0.2 为什么不等于 0.3
      • JS中的位运算
      • 稀疏数组与稠密数组
      • this到底指向谁
      • JS中的比较规范
      • 连续赋值
      • 跨域及解决方案
      • 节流和防抖
      • 从输入 URL 到页面展示,这中间发生了什么?
    • 基础

    • js
    • JS相关内容
    江月何年初照人
    2021-03-22

    从深拷贝看JS中的循环引用

    # 循环引用

    在处理 拷贝 函数时,我们要关注几个点

    1. 循环引用
    2. 相同引用
    3. 各种数据类型

    如果不处理这些,是没有办法完成深拷贝的操作的,你可能会想,我可以使用 JSON.parse (opens new window) 和 JSON.stringify (opens new window) 来处理啊

    使用 JSON 来进行转换,在日常工作中,确实比较常见,但是它也是有限制的,

    1. 如果对象中存在时间对象,在通过 JSON.stringify 和 JSON.parse 转换后,时间会变成字符串形式,而不是时间对象

      const obj = {
        date: new Date
      }
      
      obj.date.getFullYear() // 2021
      
      const temp = JSON.parse(JSON.stringify(obj))
      
      temp.date.getFullYear() // TypeError: temp.date.getFullYear is not a function
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
    2. 如果对象中存在 RegExp 、 Error 对象,序列化结果只会得到空对象

      const obj = {
          reg: new RegExp('\\d'),
          err: new Error('Test')
      }
      const temp = JSON.parse(JSON.stringify(obj)) // { reg: {}, err: {} }
      
      
      1
      2
      3
      4
      5
      6
    3. 对于循环引用的对象,会抛出 TypeError ("cyclic object value")

    还有很多中情况,JSON.stringify 都没有办法完全处理,关于 JSON.stringify 具体可参见 MDN 中对于JSON.stringify的描述 (opens new window)

    我们这次首先看看对于 循环引用和相同引用,在不使用 JSON.stringify 的情况下怎么处理

    # 首先,什么是循环引用,什么是相同引用

    我们以数组举例

    const a = [1]
    a.push(a)
    
    1
    2

    我们让 a 自己 push 了自己,这个时候就出现了循环引用

    在这种情况下, a 可以无限展开,我们如果要写递归的话,也是不能处理的,递归这里就会造成死循环。

    var arr = [1,2,3]
    var obj = {}
    obj.a = arr
    obj.b = arr
    
    1
    2
    3
    4

    我们让对象中的元素指向同一内存空间,这个时候就造成了相同引用

    # 实现一个浅拷贝

    const a = [{a: 1}]
    
    const {length} = a
    
    const b = new Array(length)
    
    let index = -1
    
    while (++index < length) {
      b[index] = a[index]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    这样就实现了一个简单的浅拷贝

    浅拷贝的问题就是在于,当引用类型的值改变时,拷贝出来的对象对应的值也会发生改变,在平时使用过程中,某些数据就是要脱离原先地址,要使用新的地址保存,使其不会污染原数据,所以就需要使用深拷贝来解决这个问题

    # 深拷贝

    我们以数组举例

    const a = [{a: 1}]
    
    1

    我们首先使用 JSON.parse 和 JSON.stringify 来完成一个深拷贝

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

    这个样子会得到我们要的结果,但是前文也说过了,如果放到循环引用和一些特定的类型时,会报错

    const a = [1]
    a.push(a)
    
    JSON.parse(JSON.stringify(a))
    
    1
    2
    3
    4

    在这个时候我们就不能使用 JSON.parse 的形式来进行 clone 了

    # 递归函数

    以数组举例

    function clone (arr) {
      if (!Array.isArray(arr)) return arr
      const {length} = arr
      const result = new Array(length)
      let index = -1
      while (++index < length) {
        result[index] = clone(arr[index])
      }
      return result
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    如果当它是数组时,我们递归处理,在上文我们说了递归处理对于普通的数组没有问题,对于循环引用的问题还是没有得到解决

    可以看到,报 栈溢出 了

    # 那么对于循环引用的问题应该怎么解决

    1. 我们可以维护一个变量用来缓存已经遍历的值
    2. 每次递归遍历时,判断当前值是否已经在变量中,如果有,说明已经递归过当前值,直接停止当前递归,返回上次递归的值即可

    以数组为例,实现如下

    function cloneDeep (arr) {
      const map = new Map
      function _deep(target) {
        if (!Array.isArray(target)) return target
    
        if (map.get(target)) return map.get(target)
    
        const {length} = target
        const result = new Array(length)
    
        map.set(target, result)
    
        let index = -1
        while (++index < length) {
          result[index] = _deep(target[index])
        }
        return result
      }
      return _deep(arr)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    通过一个 Map 对象来缓存,在递归调用时,进行一个判断,如果 key 值已经重复了,直接返回对应的 value 即可

    如果有处理 数组或者对象等引用类型对比的函数 比如 lodash 的 equalArrays (opens new window) 函数,也是这样的思路来进行循环引用的处理,可以使用 == 来进行对比

    == 会遵循 Abstract Equality Comparison (opens new window) 判断逻辑,在类型相同时,会执行 Strict Equality Comparison (opens new window) 判断逻辑

    最终如果都是引用类型会走到 SameValueNonNumber (opens new window) 规范,该规范规定 如果 x 和 y 指向同一个对象,返回 true, 否则返回 false

    至此,就解决了循环引用和相同引用的问题

    关于其他类型的处理逻辑,可以参考 lodash baseClone 的实现 (opens new window)

    编辑 (opens new window)
    #JS#Object
    上次更新: 2021/03/23 14:59:38
    平方求幂(快速幂) 算法
    0.1+0.2 为什么不等于 0.3

    ← 平方求幂(快速幂) 算法 0.1+0.2 为什么不等于 0.3→

    最近更新
    01
    a标签下载限制
    08-08
    02
    必应每日一图
    05-27
    03
    小球碰壁反弹动画
    05-26
    更多文章>
    Theme by Vdoing | Copyright © 2021-2023 Himawari | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式