lodash源码分析之equalByTag

本文为读 lodash 源码的第二百一十六篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash

gitbook也会同步仓库的更新,gitbook地址:pocket-lodash

依赖

import eq from '../eq.js'
import equalArrays from './equalArrays.js'
import mapToArray from './mapToArray.js'
import setToArray from './setToArray.js'

《lodash源码分析之eq》 《lodash源码分析之equalArrays》 《lodash源码分析之mapToArray》 《lodash源码分析之setToArray》

源码分析

equalByTag 会根据 Object.prototype.toString.call 得到的结果 tag 来判断数据类型,根据不同的数据类型来比较 objectother 是否相等。

equalByTag 的入参在《lodash源码分析之equalArrays》已经有详细的叙述,这里不再重复。

前置知识

在进入源码分析之前,需要温习一下 switch case 的一点知识。

如果 switch 命中了 case 的一个条件,而这个条件又没有写 breakreturn 语句,则程序会继续执行后面的 case 语句,直至遇到第一个 break 或者 return 语句为止。

如:

const type = 1
switch (type) {
  case 1:
    console.log(1)
    break
  case 2:
    console.log(2)
    break
  case 3:
    console.log(3)
    break
}

这段代码只会打印出 1 ,而:

const type = 1
switch (type) {
  case 1:
    console.log(1)
  case 2:
    console.log(2)
    break
  case 3:
    console.log(3)
    break
}

这段代码会打印出 12 ,因为 case 2break 语句,所以执行到这里就终止了。

一些常量

const COMPARE_PARTIAL_FLAG = 1
const COMPARE_UNORDERED_FLAG = 2

const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'

const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'

const symbolValueOf = Symbol.prototype.valueOf

源码开始之前,枚举了将要用到的 tag ,保存了 Symbol.prototype.valueof 函数。

函数主体

switch (tag) {
    ...
}
return false

可以看到,整个函数就是一个 switch case ,如果 case 处理完后没有结果,默认返回的是 false ,表示不相等。

比较 DataView

case dataViewTag:
  if ((object.byteLength != other.byteLength) ||
      (object.byteOffset != other.byteOffset)) {
    return false
  }
  object = object.buffer
  other = other.buffer

之前的文章有说过,DataViewArrayBuffer 的视图,因此 DataView 相等,首先 byteLength 要一致,即所分配的内存长度要一致;byteOffset 也要一致,即两个 DataView 开始位置的偏移量是否一致。

如果不一致,则返回 false

比较完这两者,还要比较两者的 ArrayBuffer 是否相等。

可以看到,在 dataViewTag 这个 case 中,只是将两个的 buffer 取出,覆盖了原有的变量,但是没有 break 语句,这是因为,接下来的 case 就是比较 ArrayBuffer ,因此将 ArrayBuffer 放到下个 case 来处理。

比较ArrayBuffer

case arrayBufferTag:
if ((object.byteLength != other.byteLength) ||
    !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
  return false
}
return true

两个 ArrayBuffer 的比较,也是先比较内存的大小是否相等,如果不相等,两个 ArrayBuffer 肯定不相等了。

如果相等,则使用 Uint8Array 视图,传给 equalFunc 来比较是否相等。

比较Bool、Date及Number

case boolTag:
case dateTag:
case numberTag:
    return eq(+object, +other)

Boolean 转换成数字时,true 会转换成 1false 会转换成 0

Date 转换成数字时,其实相当于 getTime ,会转换成对应时间的毫秒数,Date 确定,转换成的数字也是确定的。

如果是 Invalid day 则会转换成 NaNNaN 在比较时是不相等的。

因此 Date 也可以转换成数字来比较。

比较Error

case errorTag:
    return object.name == other.name && object.message == other.message

可以看到,只要 Error 对象的 namemessage 是相等的,就认为这两个 Error 对象相等。

比较RegExp及String

case regexpTag:
case stringTag:
    return object == `${other}`

将正则转换成字符串比较,如果转换后的字符串相等,则认为这两个正则相等。

比较Map

case mapTag:
    let convert = mapToArray

可以看到,这里也没有 break ,在这个 case 中,也没有进行 Map 的比较。

后面会看到,MapSet 一样,都是将值转换成数组,再比较数组的,因为 Map 的比较会在下一个 case 中,和 Set 的比较使用相同的逻辑。

比较Set

case setTag:
const isPartial = bitmask & COMPARE_PARTIAL_FLAG
convert || (convert = setToArray)

if (object.size != other.size && !isPartial) {
  return false
}
// Assume cyclic values are equal.
const stacked = stack.get(object)
if (stacked) {
  return stacked == other
}
bitmask |= COMPARE_UNORDERED_FLAG

// Recursively compare objects (susceptible to call stack limits).
stack.set(object, other)
const result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack)
stack['delete'](object)
return result

可以看到 Set 转换成数组的函数使用的是 setToArray ,因此会对 MapSet ,转换函数是不一样的,但最终通过转换函数 convert 得到的结果都是数组。

在进行转换和数组比较之前,先比较了两都的长度,如果长度不一致,而又没开启局部比较,则认为两者不相等。

MapSet 因为值和 key 都可能会有复杂的数据结构,因此这里使用 stack 来处理循环引用的问题,这个在《lodash源码分析之equalArrays》也有了详细的分析,这里的过程是一致的。

因为 MapSet 通过 convert 转换函数后,都是数组,因此,只需要调用 equalArrays 比较得到结果即可。

因为 MapSet 是无序的,因此比较两个数组也要使用无序比较的方式。

比较Symbol

case symbolTag:
  if (symbolValueOf) {
    return symbolValueOf.call(object) == symbolValueOf.call(other)
  }

如果有 symbolValueOfSymbol.prototype.valueOf ,则使用 symbolValueOf 来获取两者的字符串标识,如果两者的标识一致,则认为是相等的。

License

署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

results matching ""

    No results matching ""