序言
众所周知, 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 复制代码
3、运行ts文件
npm i ts-node@8.4.1 -g ts-node demo.ts 复制代码
4、创建ts项目
npm init -y tsc --init npm i ts-node -D npm i typescript -D 复制代码
四、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 } } 复制代码
上面这种方法被称为是 typeof
语法做类型守护。
3) 继续,我们来看另外一种做类型守护的方法, in
语法 。具体代码如下:
interface Bird { fly: Boolean; sing: () => {}; } interface Dog { fly: boolean; bark: () => {}; } function trainAnimal(animal: Bird | Dog) { if('sing' in animal) { animal.sing(); }else { animal.bark(); } } 复制代码
在上面的 'sing' in animal
中,我们就可以判断到 animal
是一个 Bird
类型,因此之后会进行 if
操作。
那如果是执行到下面的 else
分支呢,因为上面判断已经是 Bird
,所以下面的 else
他就会去往 Dog
类型找,之后也就可以找到 bark
方法。
五、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
的奇妙之处。
4、抽象类
(1)抽象类是什么
所谓抽象类,就是把共用性的一些东西给抽离出来,也就是把通用性的一些东西做一些封装。
(2)举个例子
我们来举个例子,看下抽象类是怎么使用的。具体代码如下:
// 抽象类 abstract class Geom { width: number; getType() { return 'demo'; } abstract getArea(): number; } class Circle extends Geom{ getArea() { return 123; } } class Squre {} class Triangle{} 复制代码
(3)抽象类和接口
有小伙伴可能已经发现,抽象类跟上面我们学到的接口似乎有点相似。那事实上,抽象类是把一些跟类相关的,通用的一些方法给抽象出来。而接口呢,是把各种跟对象和属性相关的给抽象出来。这是他们两者的区别,但它们的本质都是去把一些通用性的东西做封装。