IsEqual In JS & TS
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 : Y1 和T2 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