目录

🏎 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')
  }
}
1
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')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 类的成员修饰符

在 TypeScript 中,类的属性和方法支持三种修饰符: publicprivateprotected

  • 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')
  }
}
1
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是私有的不能读写,只能在同一个类中读写
1
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 {}
1
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)
1
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()
1
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)
1
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')
  }
}
1
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
}
1
2
3
4

对象类型的另一种声明方式就是通过接口来声明

interface Point{
  x: number,
  y: number
}
1
2
3
4

# 可选属性

在接口中可以使用 ? 定义可选属性:

interface Person {
  name: string
  age?: number
	friend?: {
    name: string
  }
}
1
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)
1
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' // 可以被修改
}
1
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'
}
1
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
}
1
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
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11
12
const stu: Student {
  sno: 110,
  name: 'simon',
 	eating: function() {
    // ...
  },
  running : function() {
    // ...
  }
}
1
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)
1
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() { 
    // ... 
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 接口 interfacetype 的区别

interfacetype 都可以用来定义对象类型,那么在开发中定义对象类型时要使用哪一个呢?

  • 如果是定义非对象类型,通常推荐使用 type ,比如 DirectionAlignment 、一些 Function
  • 如果是定义对象类型,那么两者是有区别的:
    • interface 可以重复地对某个接口来定义属性和方法;
    • type 定义的是别名,别名是不能重复的

🌰 例子:

interface IPerson {
	name: string
  running:() => void
}

interface IPerson {
  age: number
}
1
2
3
4
5
6
7
8

type 不可以重名

type IPerson {
	name: string
  running:() => void
}

// 错误用法
type IPerson {
  age: number
}
1
2
3
4
5
6
7
8
9

# 字面量赋值

interface IPerson {
  name: string
  eating:() => void
}

const p: IPerson = {
  name: "why",
  age:18, // 不存在所以报错
  eating: function() {}
}
1
2
3
4
5
6
7
8
9
10

这是因为 TypeScript 在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制

但是现将一个对象声明(变量标识符),然后再赋值给一个有类型限制的变量,TypeScript 会进行擦除操作(freshness)。

const obj = {
  name:"why",
  age: 18,
  eating: function() {
  }
}

const P: IPerson = obj
1
2
3
4
5
6
7
8
📢 上次更新: 2022/09/02, 10:18:16