今天继续更新一篇ts的学习笔记。虽然懒惰,但是我得坚持每周一次更新啊。目测这个系列更新完后,可能会更新一些Vue3新特性。我尽量把知识点单独抽象出来,然后用我自己的话进行挖掘。OK,今天来讲讲这几点。泛型还蛮重要的,所以放到这最后一篇来讲。
1.交叉类型
之前讲过“联合类型”,这是一种由“几种可能出现的类型”组成的类型,使用的符号是"|",是“或”的符号,可以看作是“或者”,等于是对这几种类型“取并集”。
我认为“交叉类型”某种程度来说跟“联合类型”的概念是可以一一对应的——它是由几种类型“合在一起”组成的类型,使用的符号是“&”,是“与”的符号,可以看作是“而且”,等于是对这几种类型“取交集”。
说实话,我一开始对“联合类型”并没有很大的疑问,但是对“交叉类型”有疑问(这也是为什么我把交叉类型放到后面来讲的原因)——“联合类型”是要把几种可能出现的类型绑定到一起,它们原本是没有交集的,所以可以写成多个类型的并集,那么“交叉类型”呢,是怎样的两个类型会有交集呢?为什么要使用交叉类型?
我现在来自己回答一下这两个问题。
怎样的两个类型会有交集呢?
提起类型,我自然就想起了诸如number,string等原始类型,显然这样的类型是不会有交集的,这就是我疑惑的原因。可是在ts中,我们也可以自己定义类型啊,上一篇博客介绍过的接口interface就很适合用来定义自定义类型。
这里的类型,我们也可以说成是“契约”,看成是对对象的要求——也就是说,接口里有什么属性,属性值是什么类型,实现这个接口的对象就得有这样的属性才符合要求。两个原本毫无交集的“契约”,如果一起作为同一类对象的要求,那么就存在交集了。
为什么要使用交叉类型呢?
假如我现在有两个不同的“契约”,分别提出了不同的要求,而我现在需要一个新的“契约”,新“契约”的要求包括了这两个“契约”的要求,我们这个时候就要使用交叉类型,来定义出一个新的类型。
interface IName {name: string
}// 把IName作为类型,并且与另一个类型(要有age属性,类型为数字)交叉在一起了
// 我们为这个交叉类型起别名叫IPerson
type IPerson = IName & {age: number}// 定义一个IPerson类型的对象
const person : IPerson = {name: 'Betty',age: 20
}
像上面这样,一个“契约”要求要有string类型的name属性,另一个“契约”要求要有number类型的age属性,这两者用&连接就定义了一个新类型,一个“既要求有string类型的name,又要求有age类型的number”的新类型,可以用type来起别名并使用。
2.泛型
这个概念非常重要。
首先,为什么要使用泛型?
假如我们的变量类型是确定的,那么在定义它的时候就可以指定出来了,就算不指定,系统也会为我们推断出一个类型。那么,假如变量在定义的时候类型并不确定呢,我们该怎么办?用any吗?不,any应该要慎用,因为使用了any后,一切天马行空的操作都可以进行,也就失去了类型的约束。也就是说,我们现在有一个难题——变量的类型未知,什么类型都可以,但是仍然需要限制类型,使用的时候必须得固定为同一个类型。这个难题可以用泛型来解决。
其次,怎么使用泛型?
先来解释一下泛型的意思——我们不知道要使用什么类型,但是又要使用这个类型,我们就可以把这个类型假设为T类型,然后等使用的时候再传入具体的类型,让T类型指代这个类型,就像函数中把实参传给形参一样。(我们要使用这个值,但又不知道这个值是多少,我们就可以把这个值设为x,然后等使用这个函数的时候再传入实参,也就是具体的值,给形参x赋值)
与函数的形参相同的是,函数使用泛型的时候也是把泛型写在函数名后面,但是括号用的是<>,并且是写在()之前的;而且泛型和形参一样可以定义多个,多个泛型的名字用逗号隔开,传递的时候注意顺序。函数的参数和返回值的类型都可以是这个泛型。
// 名字后面加<>,里面写上泛型的名字,习惯称为T
// 这个T可以作为参数和返回值的类型
function echo<T>(args: T): T {return args
}// 可以定义多个泛型,起不同的名字,然后参数和返回值就可以使用了,注意类型一一对应
function swap<T, U>(tuple: [T, U]): [U, T] {// 返回的是交换两个元素顺序的jiegreturn [tuple[1], tuple[0]]
}
与函数的形参不同的是,泛型不仅仅可以用在函数中,甚至可以用在其他需要使用类型的地方,如接口interface和类class也可以使用泛型。
在定义class或interface的时候,可以在类名或接口名后面用<T>来定义泛型,然后这个类型就可以在类和接口的内部使用了,并且具有约束性。在使用它们来定义实例的时候传入具体类型,之后如果使用的时候类型不一致就会报错,如指定了类型为number,但是把queue.push(2)改为queue.push('2')就会报错。
// 新建一个队列的类
class Queue<T> {private data = []// 向后追加一个元素,入队是从后面加入push(item: T) {this.data.push(item)}// 向前删除一个元素的方法,出队是从前面删除,返回删除的元素pop() {return this.data.shift()}
}// 创建一个新的队列实例,并且往里面添加一些数据
const queue = new Queue<number>()
queue.push(1)
queue.push(2)
最后,怎么对泛型加以约束?
有的时候我们虽然不固定要使用什么类型,可是我们需要做一些事情,我们使用的类型就必须满足某个条件才可以。
例如我们定义了一个函数,传入一个参数,然后在里面需要用到参数的length属性,那么我们的参数就必须具有length属性才可以。我们可以把“必须具有length属性”的要求写成一个“契约”,然后用这个“契约”来约束泛型。在定义泛型的时候,使用extends关键字来指定要满足的“契约”。
// 我们要在函数里使用length属性,所以要约束泛型,要求只能传入含有length属性的类型
// 要约束,所以就要使用interface接口来定义契约
interface IWithLength {// 接口里包含属性length,值类型为numberlength: number
}// 定义一个函数,传入T类型,返回T类型,而且要使用length属性
// 我们要为这个T加上约束,与上面的接口有关,定义泛型的时候使用extends关键字加约束
// 必须满足这个接口条件的类型才能传入
function echoWithLength<T extends IWithLength>(arg: T): T {console.log(arg.length)return arg
}
3.声明文件
我们使用的变量如果在ts中已经声明过了,那么我们就可以使用它,尽管它可能并不是在当前文件声明的。
什么是声明文件?声明文件,顾名思义就是用来写声明语句的文件。声明语句的格式为:declare var 变量名: 类型,它会告诉系统,这个变量的名字和类型,系统认识后,我们就可以使用了。声明文件的后缀名是.d.ts,这里的d是define,“声明”的意思。在声明文件中声明过的变量,在.ts的文件中即可使用。
使用第三方依赖的时候,需要使用到的变量很多很多,那么对应的声明语句也很多,我们写不过来怎么办?可以用npm来下载第三方提供的相应的声明文件。一般名字以"@types/"开头,如"@types/jquery"是jQuery的声明文件。在默认情况下,所有可见的@types包都会在编译的过程中被包括进来,让系统认识里面声明的变量。有的库在安装的时候也会一起下载对应的声明文件,不需要我们另外安装。
为什么ts会认识内置类型,如DOM节点类型,Math,Array呢?这是因为我们已经有一些系统内置的声明文件了,是我们的编译器自带的,以"lib."开头,涵盖了各个ES版本。
好了,关于ts的知识,我大概就知道这么多。如果以后有发现还有什么可以补充的,我也会再写一篇。欢迎志同道合的小伙伴进来学习与交流。