🏎 TypeScript 中的类与接口
# 类的使用
在早期的 JavaScrip t 开发中(ES5)我们需要通过函数和原型链来实现类和继承,从 ES6 开始,引入了 class
关键字,可以更加方便的定义和使用类。
TypeScript 作为 JavaScript 的超集,也是支持使用 class
关键字的,并且还可以对类的属性和方法等进行静态类型检测。
实际上在 JavaScript 的开发过程中,我们更加习惯于函数式编程:
- 比如 React 开发中,目前更多使用的函数组件以及结合 Hook 的开发模式;
- 比如在 Vue3 开发中,目前也更加推崇使用 Composition API。
但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。
# 类的定义
通常会使用 class 关键字:
- 在面向对象的世界里,任何事物都可以使用类的结构来描述;
- 类中包含特有的属性和方法。
🌰 定义一个 Person
类:
- 声明类的一些属性,在类的内部,声明类的属性以及对应的类型。
- 如果类型没有声明,那么它们默认是
any
类型的。 - 也可以给属性设置初始化值。
- 在默认的
strictPropertyInitialization
模式下面,类属性是必须初始化的,如果没有初始化,那么编译时就会报错。 - 如果在
strictPropertyInitialization
模式下确实不希望给属性初始化,可以使用name!: string
语法。
- 如果类型没有声明,那么它们默认是
- 类可以有自己的构造函数
constructor
,当通过new
关键字创建一个 实例时,构造函数会被调用;- 构造函数不需要返回任何值,默认返回当前创建出来的实例。
- 类中可以有自己的函数,定义的函数称之为方法。
class Person {
name!: string
age: number
constructor(name: string, age: number) {
// this.name = name // 不希望name初始化时
this.age = age
}
running() {
console.log(this.name + 'running')
}
eating() {
console.log(this.name + 'eating')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 类的继承
面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。
使用 extends
关键字实现继承,子类中使用 super
来访问父类。
🌰 例子: Student
类继承 Person
类:
class Student extends Person {
sno: number
constructor(name: string, age: number, sno: number) {
super(name, age)
this.sno = sno
}
studying() {
cosole.log(this.name + ' studying')
}
eating() {
console.log('student eating')
}
running() {
super.running();
console.log('student running')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 类的成员修饰符
在 TypeScript 中,类的属性和方法支持三种修饰符: public
、 private
、 protected
:
public
修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public
的;private
修饰的是仅在同一类中可见、私有的属性或方法;protected
修饰的是仅在类自身及子类中可见、受保护的属性或方法;
🌰 例子:
class Person {
protected name: string
constructor (name: string) {
this.name = name;
}
}
class Student extends Person {
constructor (name: string) {
super (name)
}
running() {
console.log(this.name + 'running')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
private name: string
constructor (name: string) {
this.name = name
}
}
const p = new Person("simon") // 报错,name是私有的不能读写,只能在同一个类中读写
2
3
4
5
6
7
8
9
# 只读属性 readonly
- 如果有一个属性不希望外界可以任意的修改,** 只希望确定值后直接使用,那么可以使用
readonly
**。
🌰 例子:
class Person {
readonly name: string
constructor (name: string) {
this. name = name
}
}
const p = new Person("simon")
console.log(p.name)
// p.name="bimon" // 只读属性不能修改
export {}
2
3
4
5
6
7
8
9
10
11
12
13
# getters/setters
在前面一些私有属性 pirate
是不能直接访问的,或者某些属性想要监听它的获取 (getter) 和设置 (setter) 的过程, 这个时候可以使用存取器。
🌰 例子:
class Person {
private _name: string
set name(newName) {
this._name = newName
}
get name() {
return this._name
}
constructor(name: string) {
this.name = name
}
}
const p = new Person('simon') // setter
p.name = 'bimon' // getter
console.log(p.name)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 静态成员
之前在类中定义的成员和方法都属于对象级别的,在开发中,有时候也需要定义类级别的成员和方法。通过 static
关键字定义静态成员。
🌰 例子:
class Student {
static time: string = '20:00'
statc attendClass(){
console.log('去上课')
}
}
cosole.log(Student.time)
Student.attendClass()
2
3
4
5
6
7
8
9
# 抽象类
继承是多态使用的前提:
- 所以在定义很多通用的调用接口时,通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法可以定义为抽象方法。
抽象方法:TypeScript 中没有具体实现的方法(方法体)。
- 抽象方法必须存在于抽象类;
- 抽象类是使用
abstract
关键字声明的类。
抽象类的特点:
- 抽象类是不能被实例的。(不能通过
new
创建实例对象) - 抽象方法必须被子类实现,否则该类必须是一个抽象类。
- 抽象类是不能被实例的。(不能通过
🌰 抽象类例子:
// 形状抽象类
abstract class Shape {
abstract getArea(): number
}
// 圆形实现形状抽象类
class Circle extends Shape {
private r: number
constructor(r: number) {
super()
this.r = r
}
getArea(){
return this.r * this.r * 3.14
}
}
// 矩形实现形状抽象类
class Rectangle extends Shape {
private width: number
private height: number
constructor(width: number, height: number) {
super()
this.width = width
this.height = height
}
getArea(){
retun this.width * this.height
}
}
const circle = new Circle(10)
const rectangle = new Rectangle(20, 30)
function calArea(shape: Shpae) {
console.log(shape.getArea())
}
calArea(circle)
calArea(rectangle)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 类的类型
类本身也是可以作为一种数据类型的。
🌰 例子:
class Person {
name: string
constructor(name: string) {
this.name = name
}
running() {
console.log(this.name + 'running')
}
}
const p1: Person = new Person('simon')
const p2: Person = {
name: 'bimon',
running: function() {
console.log(this.name + ' running')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 接口的声明
在 TypeScript 可以通过 type
关键字声明一个对象类型:
type Point = {
x: number
y: number
}
2
3
4
对象类型的另一种声明方式就是通过接口来声明:
interface Point{
x: number,
y: number
}
2
3
4
# 可选属性
在接口中可以使用 ?
定义可选属性:
interface Person {
name: string
age?: number
friend?: {
name: string
}
}
2
3
4
5
6
7
// 创建对象时可以使用接口定义的类型
const person: Person {
name: 'simon',
age: 3
}
console.log(person.name)
console.log(person?.age)
console.log(person?.friend?.name)
2
3
4
5
6
7
8
9
# 只读属性
在接口中可以使用 readonly
定义只读属性,意味着这个类型的属性值不可以被修改。
interface Person {
readonly name: string
age?: number
readonly friend?: {
name: string
}
}
const person: Person {
name: 'simon',
age: 3,
friend: {
name: 'bimon'
}
}
person.name = 'bimon' // 不可被修改
// 但是
if(person.friend) {
person.friend.name = 'cimon' // 可以被修改
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 索引类型
前面用 interface
来定义对象类型,这个时候其中的属性名、类型、方法都是确定的,但是有时候会遇到不确定属性名称的类型。可以定义索引类型解决。
🌰 例子:
// 定义索引类型
interface FrontLanguage{
[index: number]: string
}
const frontend: FrontLanguage = {
1: 'HTML',
2: 'CSS',
3: 'JavaScript'
}
2
3
4
5
6
7
8
9
10
interface LanguageBirth {
[name: string]: number
Java: number
}
const language: LanguageBirth {
Java: 1995,
"JavaScript": 1996,
"C": 1972
}
2
3
4
5
6
7
8
9
10
# 函数类型
前面都是通过 interface
来定义对象中普通的属性和方法的,实际上它也可以用来定义函数类型。
🌰 例子:
interface CalFunc {
(num1: number, num2: number): number
}
const add: CalFunc = (num1, num2) => {
return num1 + num2
}
const sub: CalcFunc = (num1, num2) => {
return num1 - num2
}
2
3
4
5
6
7
8
9
10
11
当然一般还是使用类型别名定义函数:
type CalFunc = (num1: number, num2: number) => number
1
# 接口集成
接口和类一样都可以被继承,也是使用 extends
关键字。同时接口是支持「多继承」的。(类不支持多继承)
interface Person {
name: string,
eating: () => void
}
interface Animal {
running: () => void
}
interface Student extends Person, Animal {
sno: number
}
2
3
4
5
6
7
8
9
10
11
12
const stu: Student {
sno: 110,
name: 'simon',
eating: function() {
// ...
},
running : function() {
// ...
}
}
2
3
4
5
6
7
8
9
10
# 接口的实现
接口定义后,也是可以被类实现的,使用关键字 implements
:
- 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;
- 这就是面向接口开发。
interface ISwim {
swimming: () => void
}
interface IRun {
runnnig: () => void
}
class Person implements ISwim, IRun {
swimming() {
console.log("swimming")
}
running() {
console.log("running")
}
}
function swim(swimmer: Iswim){
swimmer.swmming()
}
const p = new Person()
swim(p)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 交叉类型
在前 TypeScript 中有联合类型:
type Alignment = 'left' | 'right' | 'center'
1还有一种类型合并(交叉类型):
- 交叉类型表示需要满足多个类型的条件。
- 使用符号
&
。type MyType = number & string // 表示 number 和 string 两种类型都要满足 // 但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型
1
2
3
在开发中,使用交叉类型通常是对对象类型进行交叉的。
🌰 例子:
interface Colorful {
color: string
}
interface IRun {
running: () => void
}
type NewType = Colorful & IRun
const obj: newType = {
color: 'red',
running: function() {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 接口 interface
与 type
的区别
interface
和 type
都可以用来定义对象类型,那么在开发中定义对象类型时要使用哪一个呢?
- 如果是定义非对象类型,通常推荐使用
type
,比如Direction
、Alignment
、一些Function
; - 如果是定义对象类型,那么两者是有区别的:
interface
可以重复地对某个接口来定义属性和方法;- 而
type
定义的是别名,别名是不能重复的。
🌰 例子:
interface IPerson {
name: string
running:() => void
}
interface IPerson {
age: number
}
2
3
4
5
6
7
8
type
不可以重名
type IPerson {
name: string
running:() => void
}
// 错误用法
type IPerson {
age: number
}
2
3
4
5
6
7
8
9
# 字面量赋值
interface IPerson {
name: string
eating:() => void
}
const p: IPerson = {
name: "why",
age:18, // 不存在所以报错
eating: function() {}
}
2
3
4
5
6
7
8
9
10
这是因为 TypeScript 在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。
但是现将一个对象声明(变量标识符),然后再赋值给一个有类型限制的变量,TypeScript 会进行擦除操作(freshness)。
const obj = {
name:"why",
age: 18,
eating: function() {
}
}
const P: IPerson = obj
2
3
4
5
6
7
8