一、认识类

在早期的 JavaScript 开发中(ES5)需要通过函数和原型链来实现类和继承

从 ES6 开始,引入了 class 关键字,可以更加方便的定义和使用类。

TypeScript 作为 JavaScript 的超集,也是支持使用 class 关键字的,并且还可以对类的属性和方法等进行静态类型检测

实际上在 JavaScript 的开发过程中,我们更加习惯于函数式编程:

  • 比如 React 开发中,目前更多使用的函数组件以及结合 Hook 的开发模式
  • 比如在 Vue3 开发中,目前也更加推崇使用 Composition API

但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们

  • 类的定义我们通常会使用 class 关键字
  • 在面向对象的世界里,任何事物都可以使用类的结构来描述
  • 类中包含特有的属性和方法

二、类的定义

定义一个 Person 类: 使用 class 关键字来定义一个类

我们可以声明一些类的属性:

在类的内部声明类的属性以及对应的类型,如果类型没有声明,那么它们默认是 any 的

我们也可以给属性设置初始化值

在默认的 strictPropertyInitialization 模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错

1
2
3
4
5
6
7
8
class Person {
name: string;
age: number;

say() {
console.log(`Hello,${this.name}`);
}
}

如果我们在 strictPropertyInitialization 模式下确实不希望给属性初始化,可以使用 name!: string 语法

类可以有自己的构造函数 constructor,当我们通过 new 关键字创建一个 实例时,构造函数会被调用

构造函数不需要返回任何值,默认返回当前创建出来的实例

类中可以有自己的函数,定义的函数称之为方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
// 赋初始值
// name: string = '';
// age: number = 20;

name: string;
age: number;
// 使用constructor
constructor(name: string, age: 20) {
this.name = name;
this.age = age;
}
say() {
console.log(`Hello,${this.name}`);
}
}
let p = new Person("xxx", 20);

三、类的特性

继承

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提

我们使用 extends 关键字来实现继承,子类中使用 super 来访问父类

看一下 Student 类继承自 Person:

Student 类可以有自己的属性和方法,并且会继承 Person 的属性和方法

在构造函数中,我们可以通过 super 来调用父类的构造方法,对父类中的属性进行初始化;

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
class Person {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log("eating");
}
}
class Student extends Person {
son: number;
constructor(name: string, age: number, son: number) {
// super调用父类构造器
super(name, age);
this.son = son;
}
// 重写 overwrite
eating() {
// 调用父类方法
super.eating();
console.log("student eating");
}
}
const stu1 = new Student("yyy", 20, 111);

console.log(stu1.name); // yyy
console.log(stu1.age); // 20
stu1.eating(); // eating //student eating

多态

父类或者接口定义的引用变量可以指向子类或者具体实现类的实例对象

由于程序调用方法是在运行期才动态绑定的,那么引用变量所指向的具体实例对象在运行期才确定

所以这个对象的方法是运行期正在内存运行的这个对象的方法而不是引用变量的类型中定义的方法。

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
class Animals {
action() {
console.log("action");
}
}
class Dog {
action() {
console.log("dog running");
}
}

class Fish {
action() {
console.log("fish swimming");
}
}

function moveAction(animals: Animals[]) {
animals.forEach((animal) => {
animal.action();
});
}
//dog running fish swimming
moveAction([new Dog(), new Fish()]);

//多态:父类引用指向子类实例对象

类的成员修饰符

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

public、private、protected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是 public 的
  • private 修饰的是仅在同一类中可见、私有的属性或方法
  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
  • public 是默认的修饰符,也是可以直接访问的
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
class Person {
// 默认为public
name: string = "xxx";
}
let p = new Person();
console.log(p.name); //xxx

class Person2 {
// private 类中才可以访问到
private name: string = "xxx";
getname() {
return this.name;
}
}
let p2 = new Person2();
// console.log(p.name); //私有属性,类中才可以访问到

console.log(p2.getname()); //xxx

class Person1 {
// protected 受保护 类中及子类在才可以访问
protected name1: string = "xxx";
getname() {
return this.name1;
}
}
let p1 = new Person1();
// console.log(p1.name); //受保护 类中和子类在才可以访问
console.log(p1.getname()); //xxx

class Stu extends Person1 {
get() {
return this.name1;
}
}
let s1 = new Stu();
console.log(s1.get()); //xxx

只读属性 readonly

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用 readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
readonly name: string = "yy";
age: number = 20;
readonly friend?: Person;
//只读属性可以在constructor中修改
constructor(name: string, friend?: Person) {
this.name = name;
this.friend = friend;
}
}
let p = new Person("xxxx");
console.log(p.name); //xxxx
// p.name = 'mmmm' // 报错,因为只读不能修改

let p2 = new Person("nnn", new Person("kkk"));
// p2.friend = {'c'} // 报错,因为只读不能修改

// 可以这样修改
console.log(p2.friend);
if (p2.friend) {
p2.friend.age = 30;
console.log(p2.friend.age); //30
}
export {};

getters/setters

前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程, 这个时候我们可以使用存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
private _name: string = "";

set name(newName) {
this._name = newName;
}
get name() {
return this._name;
}
}
let p = new Person();
p.name = "ccc";
console.log(p.name); //ccc

export {};

静态成员

前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法

在 TypeScript 中通过关键字 static 来定义:

1
2
3
4
5
6
7
8
9
10
11
class Person {
static time: string = "10:10";

static get() {
return this.time;
}
}
console.log(Person.time); //10:10
console.log(Person.get()); //10:10

export {};

抽象类 abstract

继承是多态使用的前提。

所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式

但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法

什么是抽象方法?

  • 在 TypeScript 中没有具体实现的方法(没有方法体),就是抽象方法。
  • 抽象方法,必须存在于抽象类中
  • 抽象类是使用 abstract 声明的类
  • 抽象类有如下的特点:
    • 抽象类是不能被实例的话(也就是不能通过 new 创建)
    • 抽象方法必须被子类实现,否则该类必须是一个抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function makeArea(shape: Shape) {
return shape.getArea();
}

abstract class Shape {
abstract getArea();
}

class Jx extends Shape {
width: number;
length: number;

constructor(width: number, length: number) {
super();
this.width = width;
this.length = length;
}
getArea() {
return this.width * this.length;
}
}
let jx = new Jx(10, 20);
console.log(makeArea(jx)); //200

类的类型

类本身也是可以作为一种数据类型的

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
name: string;

eat() {}
}
//根据类型推导 p的类型为Person
let p = new Person();

let p1: Person = {
name: "xxx",
eat: function () {},
};