likes
comments
collection
share

IsEqual In JS & TS

作者站长头像
站长
· 阅读数 68

JS

日常编写业务逻辑中,我们常常需要比较两个值是否相等。然而,JS中存在诸多基本数据类型和引用数据类型的交叉使用,而又因为引用数据类型的存储方式的特点等等,常规的三等号,是不太能满足我们的日常使用的,我们常常需要去利用注入Lodash等第三方库里提供的isEqual方法,才能放心的知道两个值是否真的完全一样。那么isEqual方法到底是如何实现的呢? 我们可以将整个比较的过程拆解开来,逐步实现:1、比较两者的数据类型,如果数据类型都不一样,则直接返回false

const isEqual = (source, target) => {
  if (typeof source !== typeof target) {
  	return false;
  }
}
  

2、如果两个都是基础数据类型时,需要甄别NaN的情况,排除之后,直接比较即可

const isEqual = (source, target) => {
	//...
  if(typeof source !== 'object' && typeof source !== 'target' ) {
    if(Number.isNaN(source) && Number.isNaN(target)) {
      return true;
    }
    return source === target;
  }
}

3、接下来处理引用数据类型,但是得先排除null这个特殊情况。之后我们可以先判断两个值,引用地址是否一样,如果一样的话这代表是同一个值。在引用数据类型中,我们分别对数组和对象进行处理。

const isEqual = (source, target) => {
	//...
  if (value === null && other === null) {
    return true;
  }

  if (value === other) {
    return true;
  }
  if (Array.isArray(source) && Array.isArray(target)) {
    // 如果两个数组长度不同返回false
    if (source.length !== target.length) {
      return false;
    }
    // 迭代数组中的每个值,然后递回地用 isEqual 比较两个值
    for (let i = 0; i < value.length; i++) {
      if (!isEqual(source[i], target[i])) {
        return false;
      }
    }

    return true;
  }
  // 如果只有一个是数组,则返回false
  if(Array.isArray(source) || Array.isArray(target)) {
      return false
  }
}

接下来是对Object的处理

const isEqual = (source, target) => {
	//...
  // 首先比较两个对象的key是否一样多
  if (Object.keys(source).length !== Object.keys(target).length) {
    return false;
  }
    // 假如两个对象有一样多的 key 透过 Object.entires 迭代过第一个对象
  for (const [k, v] of Object.entries(source)) {
    // 如果第一个对象中的某个 key 不存在于第二个对象,代表两者不同
    if (!(k in other)) {
      return false;
    }
    // 递归比较
    if (!isEqual(v, other[k])) {
      return false;
    }
  }
  return true
}

TS

在学习完JS中的两个变量的比较后,我们再来看看TS中的类型比较。在TS中,我们尝尝需要比较两个类型是否相等,我们可以基于Extends和泛型来实现Ts中的isEqual。在实现之前,我们先来了解一下Extends 关键字在Typescript中的具体用法:- 继承T1和T2两个接口,分别定义了name属性和sex属性,T3则使用extends使用多重继承的方式,继承了T1和T2,同时定义了自己的属性age,此时T3除了自己的属性外,还同时拥有了来自T1和T2的属性。

 interface T1 {
    name: string
  }
  
  interface T2 {
    sex: number
  }
  
  // 多重继承,逗号隔开
  interface T3 extends T1,T2 {
    age: number
  }
  
  // 合法
  const t3: T3 = {
    name: 'xiaoming',
    sex: 1,
    age: 18
  }

-条件判断在extends条件判断中,如果extends左边的类型能够赋值给extends右边的类型,那么表达式判断为真,否则为假,这里面可能还涉及到了Ts的协变以及逆变的相关知识。

  // 示例1
  interface Animal {
    eat(): void
  }
  
  interface Dog extends Animal {
    bite(): void
  }
  
  // A的类型为string
  type A = Dog extends Animal ? string : number
  
  const a: A = 'this is string'

那么,我们该如何利用extends关键字来帮我们实现isEqual方法呢?

互为父子类型?

如果比较的两个类型互相都是对方的父子类型,不就代表两个类型相等吗?那么是否可以这样来实现呢?

type IsEqual<T, Q> = T extends Q ? Q extends T ? true : false : false

type test1 = IsEqual<1, 1> // false
type test2 = IsEqual<{ key: number }, { key?: number }> // false
type test3 = IsEqual<{ key: number }, { key: number }> // true

乍一看貌似没什么问题,但是如果我们传入的类型涉及到联合类型时,则会暴露问题了。

type test4 = IsEqual<1 | 2, 1> // boolean

这是为什么呢?原因是在于,当extends左边是一个联合类型时,就会触发分布式条件类型,左边的联合类型会依次传入每一个类型到条件判断中,得到的结果再组合为联合类型,也就是

IsEqual<1, 1> => true
IsEqual<2, 1> => false

true | false => boolean

避免分布式联合类型

那我们应该如何避免这种情况呢?我们可以利用数组,来避免分布式条件类型的发生,也就是

type IsEqual<A, B> = [A, B] extends [B, A] ? true : false


type test5 = IsEqual<1 | 2, 1> // false

但是,这样的实现方式终归还是存在缺陷的,例如无法识别any,因为在TypeScript中,any可以赋值给任何类型,同时也能被任何类型赋值,所以在上述的IsEqual里的条件判断中是恒成立的。

type test5 = IsEqual<any, number> // true

最终形态

在Typescript的Conditional Type处理中,如果extends两边都是条件类型,分别是 T1 extends U1 ? X1 : Y1T2 extends U2 ? X2 : Y2时,也就是

type X1 = something
type X2 = something

type Y1 = something
type Y2 = something

type Example1<T1, U1> = T1 extends U1 ? X1 : Y1 

type Example2<T2, U2> = T2 extends U2 ? X2 : Y2

type Final = Example1 extends Example2 ? true: false

如果Final是true的话,则代表 T1 extends T2, X1 extends X2, Y1 extends Y2,而U1等于U2。所以,如果要处理any这种类型的判断时,我们需要将extends两边都化为例子中的条件类型,然后再进行判断。也就是下面的这个最终版本:

type IsEqual<A, B> = 
	(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ?
    	true : false

代码中,我们构造了两个条件类型,分别是(() => T extends A ? 1 : 2)和(() => T extends B ? 1 : 2), 这里除了A和B不同之外,所有的值都是相同的,所以只要A和B相等,则会触发我们上述的条件类型判断公式,进而就可以利用这个一点来对两个类型进行判断了。

转载自:https://juejin.cn/post/7258469240734040125
评论
请登录