众所周知, js
是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地, bug
就UpUp了。于是,在过去的这两年, ts
悄悄的崛起了。
周一随着一波热潮,也开始进入了 ts
的世界,不得不感叹 ts
的静态美。
下面的文章中将讲解我对 TS
入门的一些归纳总结。一起来了解一下吧!
一、什么是TypeScript?
1、编程语言的类型
动态类型语言(Dynamic Typed Language) | 静态类型语言(Statically Typed Langeage) |
---|---|
JavaScript | C,C++,C#,JAVA |
2、TypeScript究竟是什么?
Typescript,即
Javascript that scales
;ts
把不看重类型的动态语言JS
转变成关注类型的静态语言;可以说ts是静态类型风格的类型系统;
从
es6
到es10
甚至是esnext
的语法支持;兼容各种浏览器,各种系统,各种服务器,完全开源。
二、为什么要学习TypeScript?
1、程序更容易理解
动态语言存在函数或者方法中其输入输出的参数类型等问题,同时,动态语言还受到各种各样的约束,比如需要手动调试等等。那么有了 ts
,代码本身就可以解决上述问题了, ts
让程序更容易理解,程序理解我们,我们就可以少干很很多事情。
就像我们在与别人交谈时,假如我们逻辑很清晰的表达给对方,对方马上听懂了,并且理解了我们,我们也很省力,不用长篇大论的介绍。
2、效率更高
ts
可以在在不同的代码块和定义中进行跳转,并且代码有补全功能。
同时, ts
还有丰富的接口提示,可以通过使用 .
来提示所有的接口内容。
3、更少的错误
ts
在编程期间,就可以发现大部分的错误。这样就可以杜绝掉一些比较常见的错误,也使得后面程序运行更加通畅。
4、非常好的包容性
ts
可以完全地兼容 Javascript
,同时,如果要引入像 JQuery
之类的第三方库时,可以单独编写类型文件来引入这些库。
5、一点小缺点
相比于 js
来讲, ts
在学习之初,需要去习惯一些规范,短期内会增加一点学习成本。但短期的学习成本增加将会使得在后期的开发当中减少很多不必要的错误和麻烦,间接的也为自己的开发带来很大的益处。
闲谈到此结束,让我们一起来进入 ts
的世界吧!
三、typescript入门
1、如何安装TypeScript
npm install -g typescript
2、查看版本号
tsc -v
四、Typescript数据类型
1、原始数据类型和Any类型
(1)原始数据类型
//定义一个布尔值数据
let isDone: boolean = false
//定义一个数字类型
let age: number = 20
//定义字符串类型
let firstName: string = 'monday'
let message: string = `Hello, ${
firstName}`
//定义undefind和null类型
let u: undefined = undefined
let n: null = null
//给数字赋值undefid
let num: number = undefined
(2)Any 类型
如果我们有时候不能确定一个数据是什么类型的话,那么我们可以用any类型来定义。比如:
//定义一个any类型数据
let notSure: any = 4
notSure = 'maybe a string'
notSure = true
notSure.myName
notSure.getName()
2、数组和元组
(1)数组
//声明一个数字类型的数组
//注意:后面的数组只能传递数字,传递其他类型的数据都会报错
let arrOfNumbers: number[] = [1, 2, 3];
arrOfNumbers.push(3)
function test(){
//arguments 为类数组
console.log(arguments)
}
(2)元组
//确定一个元组里面的内容和数量,下面表示确定user这个元组必须且只能接收两个参数
//同时第一个属性必须是String类型,第二个属性是Number类型
let user: [String, Number] = ['abc', 13]
3、interface 接口
interface的定义:
- 对
对象Object
的形状(shape)
进行描述; - Duck Typing(鸭子类型)。
我们来看一段代码:
interface Person{
// readonly表示只读状态
readonly id: number,
name: String,
//加一个问号表示该参数可选可不选
age?: number
}
let monday: Person = {
id: 1,
name: 'monday',
age: 18
}
monday.id = 12323; //因为加了readonly,所以此时访问不了,会报错
4、function函数
function函数是什么:
- 在
JS
中,函数是一等公民。 - 函数和其他类型的对象都一样,可以作为参数,可以存入数组,也可以被另外一个函数返回,可以被赋值给另外一个变量。
- 函数主要由两个部分组成:输入(传参)和输出(返回结果)。
我们来看个例子:
function add(x: number, y: number, z?: number): number{
if(typeof z === 'number'){
return x + y + z;
}else{
return x + y;
}
}
let result = add(1, 2, 3);
console.log(result); //6
通过以上函数,我们实现了两个树或者三个树的相加操作。此时,需要我们注意的是,可选参数后面不能再添加不确定参数,否则程序就会发生混乱。比如:
function add(x: number, y: number, z?: number, t: number): number{
if(typeof z === 'number'){
return x + y + z;
}else{
return x + y;
}
}
以上代码中的 t
是肯定不被允许添加的,因为前面已经有了可选参数 z
,而后面又突然健冒出来个 t
,想想都不太合理。
到这里,假设我们由一个新的变量名,名字叫 add2
。这个时候我们想要给它像 add
函数一样的类型。那么该怎么处理呢?
let add2: (x:number, y:number, z?: number) => number = add
注意上面这个箭头 =>
不是 ES6
中的箭头函数,而是 ts
中声明函数类型返回值的方法。
上面这个语句中就说明了, add2
返回的值是一个 number
类型数值,并且让它等于 add
函数。同时,要记得的是,在 ts
当中,凡是在 :
后面都是声明在声明类型。
上面这样写好像有点冗余,我们来用 interface
来实现同样的效果。
在第3点的 interface
中我们了解到, interface
是对对象的形状进行描述,但值得注意的是, interface
也可以是对函数的形状进行描述。我们用代码来实现一下。
interface ISum {
(x: number, y: number, z?: number) : number
}
let add2: ISum = add
通过以上代码,我们看到,用 interface
来封装一个函数的返回值来行,看起来优雅了不少。这里先体会一下, interface
的强大之处,在后面还会继续讲解。
5、类型推论、联合类型和类型断言
(1)类型推论
有时候我们还没有给一个数据定义类型,就直接给它赋值了。这个时候我们要怎么来判断呢。这个数据的类型呢?
比如:
let str = 123
当出现这样的情况时,编译器会直接给 str
赋上 number
类型。那么此时如果我们想这么干:
let str = 123
str = 'asd' //会报错
结果当然时不行的。当第一次赋值的时候,编译器就已经给 str
一个 number
类型,认定 str
就是 number
类型。而后我们还想要给 str
赋值上一个 string
类型的数据,肯定是会报错的。
(2)联合类型
有时候我们对一个数据的类型不够确定,比如说不知道某一个数据它是 number
类型还是 string
类型。这个时候我们就可以用联合类型来进行一波操作。
let numberOrString: number | string
通过这种方式,我们对我们所定义的属性 numberOrString
进行联合类型操作。
一般情况下,联合类型会结合类型断言来进行使用。接下来我们来讲类型断言。
(3)类型断言
1) 当 TypeScript
不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法,而有时候呢,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法。因此我们采用类型断言的方式将其指定为一个类型。(这么做只是先欺骗了 ts
,让其信任我们所指定的类型)
let str = 123
function getLength(input: string | number) : number{
// 用as对input进行类型断言,先给input指定一个类型,后面判断不是再进行转换
//注意:类型断言只做类型选择,而不做类型转换
const str = input as string
console.log(typeof str)
if(str.length){
return str.length
}else{
const number = input as number
return number.toString().length
}
}
2) 看到这里,已经开始感觉到类型断言的神奇之处。但用上面这种方法感觉好像还有一点点冗余,于是我们引入一个 type guard
,即类型守护。我们来看下实现方式。
let str = 123
function getLength2(input: string | number): number{
if(typeof input === 'string'){
return input.length
}else{
return input.toString().length
}
}
五、Typescript中的类:class
在 js
中我们用了构造函数和原型链的方式来实现继承,同时在 ES6
中出现了 class
类继承的方法。那在 typescript
中呢,继承的方法又更加丰富了。让我们一起来一探究竟吧!
1、类的定义
我们先来看下类的定义。
(1)类(Class)
类定义了一切事物的抽象特点,包含它的属性和方法。比如:
class Animal{
// 构造函数是实例化执行时候的逻辑
constructor(name){
this.name = name
}
run(){
return `${
this.name} is running`
}
}
阅读以上代码我们可以知道,通过 class
可以定义一个类。
(2)对象(Object)
对象 Object
,就是类的实例。举个例子: 🙆♂️
我们可以把类 class
比喻成一张蓝图,比如说汽车是一个 class
,那么它就像是一张造汽车的图纸。第二个是 Object
, Object
通过 new
生成,那么前面有了汽车的蓝图,我们现在就可以创造实实在在的汽车了。我们可以说一辆特斯拉是汽车的实例,也可以说宝马是汽车的另外一个实例。
同样我们用上面的例子来做衍生。具体如下:
class Animal{
// 构造函数是实例化执行时候的逻辑
constructor(name){
this.name = name
}
run(){
return `${
this.name} is running`
}
}
const snake = new Animal('lily')
console.log(snake.run())
阅读以上代码我们可以知道,我们定义了一个 snake
,这个 snake
继承了 Animal
类,因此它就可以用 Animal
类的属性和方法。
此时打印结果如下:
(3)面向对象(OOP)的三大特性
面向对象的三大特性分别为:封装、继承、多态 。
- 封装: 指将数据的操作细节隐藏起来,只暴露对外的接口。那这样子的话,对于外界的调用端来说,他们不需要也不可能知道细节,只能通过对外的接口来访问该对象。
- 继承: 子类可以继承父类,子类除了拥有父类的所有特征外,还会拥有一些更具体的特性。
- 多态: 由继承产生的相关不同的类,对同一个方法可以有不同的响应。比如,猫和狗,他们都可以继承
Animal
类,但是他们分别实现run()
方法,此时呢,针对某一个实例,我们无需了解它是猫还是狗,这个时候可以直接调用run()
,程序会自动判断出来,应该如何去执行这个方法。
同样,我们用上面的代码做衍生,来看继承和多态是怎么样的。
继承:
class Animal{
// 构造函数是实例化执行时候的逻辑
constructor(name){
this.name = name
}
run(){
return `${
this.name} is running`
}
}
const snake = new Animal('lily')
// console.log(snake.run())
class Dog extends Animal{
bark(){
return `${
this.name} is barking`
}
}
const xiaoqi = new Dog('xiaoqi')
console.log(xiaoqi.run())
console.log(xiaoqi.bark())
此时打印结果如下:
从上面可以看到, Dog
继承了 Animal
类,此时 Dog
就拥有了 Animal
类的属性和方法。而 xiaoqi
实例化了 Dog
,因此它也拥有 Dog
的属性和方法。
多态:
class Animal{
// 构造函数是实例化执行时候的逻辑
constructor(name){
this.name = name
}
run(){
return `${
this.name} is running`
}
}
const snake = new Animal('lily')
// console.log(snake.run())
//-----------------------------------
class Dog extends Animal{
bark(){
return `${
this.name} is barking`
}
}
const xiaoqi = new Dog('xiaoqi')
console.log(xiaoqi.run())
console.log(xiaoqi.bark())
//-----------------------------------
class Cat extends Animal{
// 静态方法不需要进行实例化,直接在类上调用即可
static categories = ['mammal']
constructor(name){
super(name)
console.log(this.name)
}
run(){
return `Meow, ` + super.run()
}
}
const maomao = new Cat('maomao')
console.log(maomao.run())
// 直接访问静态属性
// 为什么要有静态属性?当定义和实例没有太大关系时,可以考虑使用静态方法实现
console.log(Cat.categories)
此时打印结果如下:
阅读代码我们可以发现, xiaoqi
继承了 dog
的 run()
方法,而 Cat
继承了 Animal
类,但是它对 run()
方法进行了改写,因此最终的 run()
方法为改写后的效果。
所以, maomao
继承了 Cat
类,最后 maomao
调用 run()
方法时,就会调用 Cat
里面改写的 run()
方法,而不是 Animal
类的 run()
方法。
这样, xiaoqi
和 maomao
虽然同样继承自 Animal
类,但他们调用 run()
方法的结果各自相互独立,如此,就实现了多态。
同时,我们还要注意一个点,就是静态属性。大家可以看到上面定义的 categories
,用了 static
来定义它为静态属性。当把变量定义为静态属性时,则当外部需要该静态方法时,不需要进行实例化,之类在类上调用即可。
那么问题来了,我们什么时候才需要有静态属性呢?
其实,当定义的内容和实例没有太大关系时,就可以考虑使用静态方法。比如常量的使用,常量基本是固定的,不会变的,所以我们可以考虑直接使用静态方法来获取它。
2、Typescript中的类
Typescript是通过什么方式来增强类的呢,typescript一般通过以下四种修饰符来增强类:
修饰符 | 含义 |
---|---|
public | 修饰的属性或方法是公有的 |
private | 修饰的属性或方法是私有的 |
protected | 修饰的属性或方法是受保护的 |
readonly | 只能读不能写 |
有了以上这四种修饰符呢,我们就可以给类的方法和属性进行权限管理。为什么要做权限管理呢?因为总有些内容,我们是不愿意暴露给外部使用的,所以需要进行权限管理。
值得注意的是,对于 protected
这个修饰符来说,只有子类可以访问父类的属性和方法,其他实例都不能访问。这其实可以把 protected
这个变量理解为遗产,父类的东西直接给子女继承,其余外部人员一概不能访问。
3、类和接口
(1)解决什么问题
继承存在着这样一个困境,在面向对象的世界中,一个类只能继承另外一个类,有时候同类之间有一些共同的特性,但是使用子类来继承父类又很难完成。于是接口就出现了。
(2)如何解决
类可以使用 implements
来实现接口,怎么做呢?我们可以把这些相同的特性提取成接口,然后用 implements
这个关键字来实现,这样就大大提高了面向对象的灵活性。
(3)举个例子
假如我们现在要让一辆汽车和一部手机来实现打开播放器的功能,那么我们会这么实现:
class Car{
switchRadio(trigger: boolean){
}
}
class CellPhone{
switchRadio(trigger: boolean){
}
}
但是这样子看起来好像就没有特别雅观。于是我们可以写一个打开播放器的接口,然后用 implements
来实现这个功能。代码如下:
interface Radio{
switchRadio(trigger: boolean): void
}
class Car implements Radio{
switchRadio(trigger: boolean){
}
}
class CellPhone implements Radio{
switchRadio(trigger: boolean){
}
}
这样,就让Car和CellPhone实现了打开播放器的功能。
接下来,我们继续写一个接口,可以实现检查电池电量的功能。并且让手机不仅可以打开播放器,还可以检查电池电量。代码如下:
interface Radio{
switchRadio(trigger: boolean): void
}
interface Battery{
checkBatteryStatus(): void
}
class Car implements Radio{
switchRadio(trigger: boolean){
}
}
class CellPhone implements Radio,Battery{
switchRadio(trigger: boolean){
}
checkBatteryStatus(){
}
}
阅读代码我们可以发现,我们要给继承两个接口 Radio,Battery
,这样看似乎还有点冗余。于是我们可以这样实现:
interface Radio{
switchRadio(trigger: boolean): void
}
interface RadioWithBattery extends Radio{
checkBatteryStatus(): void
}
class Car implements Radio{
switchRadio(trigger: boolean){
}
}
class CellPhone implements RadioWithBattery{
switchRadio(trigger: boolean){
}
checkBatteryStatus(){
}
}
通过 interface
继承 interface
,最终用 implement
去抽象和验证类的属性和方法,达到抽离功能的目的。
相信通过以上的简单了解,大家能感受到一点 interface
的奇妙之处。
六、枚举
1、普通枚举
枚举常使用于我们在程序中需要做权限管理或者做判断时等各种场景。枚举比较简单,下面直接用代码演示:
enum Direction{
Up,
Down,
Left,
Right
}
console.log(Direction.Up) //0
console.log(Direction.Down) //1
console.log(Direction.Left) //2
console.log(Direction.Right) //3
console.log(Direction[0]) //Up
除了以上基本用法外,我们还可以给枚举赋值:
enum Direction{
Up = 10,
Down,
Left,
Right
}
console.log(Direction.Up) //10
2、常量枚举
我们来定义一个常量,与 enum
做判断。
enum Direction{
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
//定义一个常量,直接与enum做判断
const value = 'Up';
if(value === Direction.Up){
console.log('go Up!') // go Up!
}
使用常量枚举可以有效地提升性能,常量会内联枚举的任何用法,而不会把枚举变成任意的 Javascript
代码。
这样一说,那是不是所有的 enum
都可以使用常量枚举呢?答案自然是否定的。
枚举的值有两种类型,一种是常量值枚举(constant),一种是计算值枚举(computed)。只有常量值枚举可以进行常量枚举,而计算值枚举不能进行常量枚举。
七、泛型
接下来我们来讲 TypeScript
中最难的一部分,泛型。
1、普通泛型
泛型,即 generics
。指在定义函数、接口或类的时候,我们不预先指定类型,而是在使用的时候再指定类型和其特征。
可以理解为,泛型就相当于一个占位符或者是一个变量,在使用时再动态的填入进去,填进去以后既可以来确定我们的类型值。
接下来我们用代码来演示一下:
function echo<T>(arg: T): T{
return arg
}
const result = echo(true)
console.log(result) // true
我们通过 <>
来定义一个未知的泛型,之后当我们给它赋值时,就可以对应值的数据类型。
现在我们再用泛型来定义一个 number
类型的数组。具体代码如下:
// 早期定义一个number类型的数组
let arr: number[] = [1, 2, 3]
// 用泛型定义一个number类型的数组
let arrTwo: Array<number> = [1, 2, 3]
假如我们现在要调换两个元素的位置,那么用泛型我们可以这么实现。具体代码如下:
function swap<T, U>(tuple: [T, U]): [U, T]{
return [tuple[1], tuple[0]]
}
const result2 = swap(['abc', 123])
console.log(result2[0]) //123
console.log(result2[1]) //abc
通过泛型,我们就顺利调换了两个元素的位置。
2、约束泛型
在泛型中使用 extends
关键字,就可以让传入值满足我们特定的约束条件,而不是毫无理由的随意传入。
比如,我们想要让我们定义的内容是一个数组,我们可以这么处理。具体代码如下:
function echoWithArr<T>(arg: T[]): T[]{
console.log(arg.length)
return arg
}
const arrs = echoWithArr([1, 2, 3])
这样,就把 arrs
定义为一个数组。
假设我们现在想要让我们定义的内容可以访问到 length
方法,那么我们需要加一点佐料。具体代码如下:
interface IWithLength{
length: number
}
function echoWithLength<T extends IWithLength>(arg: T): T{
console.log(arg.length)
return arg
}
const str = echoWithLength('str')
const obj = echoWithLength({
length: 10, width: 20 })
const arr2 = echoWithLength([1, 2, 3])
通过 extends
关键字来继承特定的 interface
,使得我们定义的内容 str
, obj
,arr2
达到可以访问length方法的目的。
通过以上举例,我们可以知道,泛型可以用来灵活的约束参数的类型,参数不需要是个特别死板的类型,而可以通过我们的约束来达到我们想要的目的。
3、泛型在类和接口中的使用
(1)泛型在类中的使用
class Queue<T>{
private data = []
push(item: T){
return this.data.push(item)
}
pop(): T{
return this.data.shift()
}
}
// 确定这是一个number类型的队列
const queue = new Queue<number>()
queue.push(1)
console.log(queue.pop().toFixed())
(2)泛型在接口中的使用
interface KeyPair<T, U>{
key: T
value: U
}
let kp1: KeyPair<number, string> = {
key: 1, value: 'str'}
let kp2: KeyPair<string, number> = {
key: 'str', value: 2}
通过以上代码演示可以发现,泛型就像是创建了一个拥有特定类型的容器,就仿佛给一个容器贴上标签一样。
八、类型别名
1、类型别名
类型别名,即 type aliase
。类型别名可以看作是一个快捷方式,可以把一个写起来很繁琐的类型创建一个简单的写法。比如:
//用以下这种写法,每次都要写一长串的(x: number, y: number) => number
let sum: (x: number, y: number) => number
const result = sum(1, 2)
//用type给类型进行别名
type PlusType = (x: number, y: number) => number
let sum2: PlusType
const result2 = sum2(2, 3)
//一个类型可以是字符串也可以是数字
type StrOrNumber = string | number
let result3: StrOrNumber = '123'
result3 = 123
2、字符串字面量
字符串字面量,指可以提供一系列非常方便使用的常量写法。比如:
const str: 'name' = 'name'
const number: 1 = 1
type Direction = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Direction = 'Left'
3、交叉类型
交叉类型,使用 type
这个扩展对象的一种方式。比如:
interface IName{
name: string
}
type IPerson = IName & {age: number}
let person: IPerson = {name: 'monday', age: 18}
九、声明文件
我们在写ts时,难免会有遇到要引入第三方库的时候。这个时候就需要ts来做特殊处理。主要有以下两种做法:
1、 .d.ts 文件引入
假设我们要引入JQuery库来使用,那么我们可以在外部新增一个 JQuery.d.ts
文件,文件内代码如下:
declare var JQuery: (selector: string) => any;
之后便可以在我们定义的 ts
文件下引用 JQuery
相关库的内容。比如:
jQuery('#foo')
2、npm安装
我们也可以安装对应的第三方库的 npm
包。假如我们现在要引入一个 JQuery
库,那么我们可以这么处理。
npm install --save @type/jquery
十、内置类型
我们在写 ts
代码时,其实不知不觉已经使用了很多的内置对象。对象呢,是指根据标准(标准指 ECMA
、 DOM
等标准),在全局作用域 global
上面存在的对象。那我们在运行 tsc
时,这些内置的对象就会被当作附加的礼物给程序加载进行。接下来我们来体会一下几种常见的内置对象。
全局对象:
// global object 全局对象
const a: Array<number> = [1, 2, 3]
const date = new Date()
date.getTime()
const reg = /abc/
reg.test('abc')
内置对象:
// build-in object 内置对象
Math.pow(2, 2)
DOM和BOM对象:
// DOM 和 BOM
let body = document.body
let allLis = document.querySelectorAll('li')
allLis.keys()
document.addEventListener('click', e => {
e.preventDefault()
})
功能性类型:
// Utility Types 功能性类型
interface IPerson{
name: string
age: number
}
let monday: IPerson = {
name: 'monday', age: 20}
//可选属性
type IPartial = Partial<IPerson>
let monday2: Ipartial = {
name: 'monday'}
//移除某一个属性
type Omit = Omit<IPerson, 'name'>
let monday3: Omit = {
age: 20}
十一、结束语
关于 ts
的入门讲到这里就结束啦!希望大家能对 ts
有一个简单的认识!
如本文有不理解或有误的地方欢迎评论区评论或私信我交流!我们下期见!
- 关注公众号 星期一研究室 ,不定期分享学习干货,学习路上不迷路~
- 如果这篇文章对你有用,记得点个赞加个关注再走哦~