前端深克隆-克隆循环体

151 阅读6分钟

什么是深克隆

简单描述,深克隆即clone出来的变量和另外一个变量

两者之间毫无关系却处处相等

形象比喻就是按照你的模样做一个双胞胎,不仅仅是模样一模一样,就是连内在也一模一样,及时你们的关系网络也是一模一样,但是却都不是一个

深克隆的最细粒度是什么

为了深入了解克隆的基本单位,我们需要完全覆盖JavaScript的数据类型,然后针对不同的类型进行处理,为了区分不同类型的数据,我们从JavaScript build-in 类型本身触发,iterate每一个类型的数据,力求涵盖所有类型,从MDN上我们可以的得到build-in数据类型有:

  • Primitive Value

    • Boolean
    • String
    • Null
    • Undefined
    • Number
    • Symbol
    • BigInt
    • Function
  • Object

以上即为JavaScript内部的所有数据类型, 这也是 typeof 关键字能够触达的最全面的数据类型,既然探讨到了typeof,那我们可以做一下延伸 typeof 本质不是一个function, 它属于JavaScript语言的一个连接上层与内存数据之间的快捷映射关系,也就是说typeof是对内存中数据的直接读取并根据数据最后三位bit进行识别,例如最后三位为 000 则表示该数据为Object类型, 这儿有一个特例 null 的类型也为 object, 原因是因为这两个数据在内存中的最低三位也是 000 为了探究为什么会这样,笔者在网络上搜索多年后没法得到一个令人信服的原因,有说设计如此,有说当初没考虑,有说 000 是万物的开端, 不一而终,换一个方式,我仅仅需要记住 000 能够指向两个类型的数据即可

所以我们可以武断地说, typeof 能够得到的结果只有8种 , 探究原因也就是上面指出的,切记这是从JavaScript的数据结构层面进行的, 所以在面试或者其他技术交流的时候需要有理有据指出深层次原因,可以不用记住每一种类型,但是却需要指出只能有8种可能,不能多,能不能少这个问题留给大家进行思考🤔

说清楚基本类型后我们开始对每种类型进行克隆单元进行编写前的设计和构造

  • Boolean
  • String
  • Null
  • Undefined
  • Number
  • Symbol
  • BigInt
  • Function
  • Object

编写前我们需要再次定义一下相等是什么概念, Strict equality (===) - JavaScript | MDN 三等意味着

  • 类型

的相等,然后我们单独分析这两个如何判断相等

  1. 类型相等

在js中,除了简单类型外,一切对象均继承于Object , 那我们先把简单类型列出来

简单类型
  1. Boolean
  • true | false, 只有两个值,很容易比较
  1. String
  • toString的返回值进行比较
  1. Null
  • null === null 为真
  1. Undefined
  • undefined === undefined 为真
  1. Number
  • Number的比较较为特殊,我们先看Number存在的值有哪些
// 获取 Number 的所有属性

0: "length"

1: "name"

2: "prototype"

3: "isFinite"

4: "isInteger"

5: "isNaN"

6: "isSafeInteger"

7: "parseFloat"

8: "parseInt"

9: "MAX_VALUE"

10: "MIN_VALUE"

11: "NaN"

12: "NEGATIVE_INFINITY"

13: "POSITIVE_INFINITY"

14: "MAX_SAFE_INTEGER"

15: "MIN_SAFE_INTEGER"

16: "EPSILON"



// 我们重点关注下 NaN, 其他的均很好比较

NaN === NaN // false

  1. Symbol
  • 复制比较后为一个对象, Symbol均不相等
  1. BigInt
  • 同Number
  1. Function
  • Function继承于Object又独立于对象体系,内部比较机制不是很清晰,可以断定是直接比较内部内存指针指向的内存地址是否一致
复合类型
  1. Object

    1. Object
    2. Array
    3. Buffer
    4. ArrayBuffer
    5. Date
    6. Regex
    7. DataView
    8. Map
    9. ...
    10. etc.

对于复合的类型来说,不同框架是尽量去利用JavaScript原生支持的对象类的迭代器循环每一个子元素进行比较,克隆过程也需要获取其constructor, 然后对constructor进行比较,找到合适的constructor后进行元素clone, 例如变量objectMap类型, lodash流程为

function clone(){



    // ........

    const Ctor = object.constructor

    const result = new Ctor

    object.forEach((subValue, key) => {

      result.set(key, clone(subValue))

    });

    return result;

    // ........

}
  1. 值相等

所有的复合类型最终都会化简为简单类型, 任何类型均继承于JavaScript中的Object,所以本质是简单类型的比较和复制,附JavaScript中的原型链关系图

思考循环引用

通过上面的探讨,我们已经深入了解了深克隆的本质其实是对不同类型的迭代器子元素的克隆直到克隆到基础元素类型就不再往下进行迭代复制,既然解释清楚了深克隆,那如果某个对象中存在循环应用,如图

如果我们复制对象A, 那么 A => C => E => A 存在着一条循环引用链

由此引出我们今天的主题

克隆循环体

如何去实现

思考上面示例中的对象A, 产生其本质是因为其子元素子元素*N 中存在一个指向本身内部的一个指针,也就是内存中栈空间和堆空间有相互的指向关系,形成了一个闭环,那如果不考虑对象的种种结构,我们简化模型,全部用树形结构对数据进行抽象flatten,其实得到的是一张图(数据结构中的图结构) ,而且是一个有向图,形如

那其实我们就可以将问题简单化,一个对象为循环体的本质为这个对象抽象的有向图中

有向图存在闭环

既然我们已经了解了克隆循环体的一些本质了,那我们现在需要根据JavaScript的特性去做一个克隆算法,能够将这种存在循环引用的对象克隆,为此我们需要这个算法能提供几个功能,让我们能够完成深克隆

  1. 针对简单类型进行复制
  2. 判断当前复制的复杂类型value是否已经在对象中的某个路径

(以上两个条件为简单化抽象后的必备要素,实际情况需考虑更多因素)

我们以下图作为循环体示例,讲解一种思路,做A对象的复制,且简化为A,C,E之间的循环依赖

初始化两个list, sourceList, targetList

步骤

  1. 初始化两个空list
  2. 第一步,复制A后,分别把原始值A和复制好的A(没有复杂类型子元素)分别放到两个列表中
  3. 复制A的依赖C, 查看C是否已经分别在列表1中,如果有,直接把值取出来赋值,没有继续
  4. 复制C的依赖E
  5. 复制E的依赖A,发现A已经在源列表1中,那直接将target指向对应位置的元素,停止复制

以上为一种简单实现,实际情况中有更为优雅的实现,限于水平,特以此为示例以增理解

深克隆的理解和解决方法中,以上仅仅是众多理解方式和实现方式中冰山一角,对深克隆的理解的意义我认为是更为重要的,提出问题的意义远远比解决问题更重要,希望能够帮助理解.

思考区

  • 是否有这样的可能,服务端传到前端的response数据就为一个循环体
  • 如何对两个循环体进行equals比较

参考资料