# 1. 类的概念
在介绍 TypeScript
中类的用法之前,我们有必要先在这里对类相关的概念做一个简单的介绍。
- 类(
Class
):定义了一件事物的抽象特点,包含它的属性和方法; - 对象(
Object
):类的实例,通过new
生成; - 面向对象(
OOP
)的三大特性:封装、继承、多态; - 封装(
Encapsulation
):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据; - 继承(
Inheritance
):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性; - 多态(
Polymorphism
):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
; - 存取器(
getter & setter
):用以改变属性的读取和赋值行为; - 修饰符(
Modifiers
):修饰符是一些关键字,用于限定成员或类型的性质。比如public
表示公有属性或方法; - 抽象类(
Abstract Class
):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现; - 接口(
Interfaces
):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(Implements
)。一个类只能继承自另一个类,但是可以实现多个接口;
# 2. 类的基本定义与使用
使用 class
定义类,使用 constructor
定义构造函数。通过 new
生成新实例的时候,会自动调用构造函数。
class Greeter {
// 声明属性
message: string;
// 构造方法
constructor(message: string) {
this.message = message;
}
// 一般方法
greet(): string {
return "Hello " + this.message;
}
}
// 创建类的实例
const greeter = new Greeter("world");
// 调用实例的方法
console.log(greeter.greet());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上面代码中,我们声明了一个 Greeter
类。这个类有 3
个成员:一个叫做 message
的实例属性,一个构造函数和一个叫做 greet
的实例方法。
你会注意到,我们在引用任何一个类成员的时候都用了 this
。 它表示我们访问的是类的成员。
后面一行,我们使用 new
构造了 Greeter
类的一个实例。它会调用之前定义的构造函数,创建一个 Greeter
类型的新对象,并执行构造函数初始化它。
最后一行通过 greeter
对象调用其 greet
方法
# 3. 类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。看下面的例子:
class Animal {
run(distance: number) {
console.log(`Animal run ${distance}m`);
}
}
class Dog extends Animal {
cry() {
console.log("wang! wang!");
}
}
const dog = new Dog();
dog.cry();
dog.run(100); // 可以调用从父中继承得到的方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog
是一个派生类,它派生自 Animal
基类,通过 extends
关键字。 派生类通常被称作子类,基类通常被称作超类。
因为 Dog
继承了 Animal
的功能,因此我们可以创建一个 Dog
的实例,它拥有 cry()
和 run()
实例方法。
下面我们来看个更加复杂的例子。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`);
}
}
class Snake extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name);
}
// 重写父类型的方法
run(distance: number = 5) {
console.log("sliding...");
super.run(distance);
}
}
class Horse extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name);
}
// 重写父类型的方法
run(distance: number = 50) {
console.log("dashing...");
// 调用父类型的一般方法
super.run(distance);
}
xxx() {
console.log("xxx()");
}
}
const snake = new Snake("sn");
snake.run();
const horse = new Horse("ho");
horse.run();
// 父类型引用指向子类型的实例 ==> 多态
const tom: Animal = new Horse("ho22");
tom.run();
/* 如果子类型没有扩展的方法, 可以让子类型引用指向父类型的实例 */
const tom3: Snake = new Animal("tom3");
tom3.run();
/* 如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends
关键字创建了 Animal
的两个子类:Horse
和 Snake
。
与前一个例子的不同之处是,派生类拥有自己的构造函数,并且在自己的构造函数中必须调用 super()
来执行基类的构造函数。 而且,在构造函数里访问 this
的属性之前,我们一定要调用 super()
。
这个例子演示了如何在子类里可以重写父类的方法。Snake
类和 Horse
类都创建了 run
方法,它们重写了从 Animal
继承来的 run
方法,使得 run
方法根据不同的类而具有不同的功能。注意,即使 tom
被声明为 Animal
类型,但因为它的值仍然是 Horse
,所以调用 tom.run(34)
时,它还是会调用 Horse
里重写的方法。
sliding...
sn run 5m
dashing...
ho run 50m
2
3
4
# 4. 存取器
TypeScript
支持通过 getter/setter
来改变属性的赋值和读取行为。 它能帮助你有效的控制对对象成员的访问。
class Person {
firstName: string = "A";
lastName: string = "B";
get fullName() {
return this.firstName + "-" + this.lastName;
}
set fullName(value) {
const names = value.split("-");
this.firstName = names[0];
this.lastName = names[1];
}
}
const p = new Person();
console.log(p.fullName);
p.firstName = "C";
p.lastName = "D";
console.log(p.fullName);
p.fullName = "E-F";
console.log(p.firstName, p.lastName);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 5. 类的静态成员
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。
静态属性, 是类对象的属性,非静态属性, 是类的实例对象的属性。
静态成员分为静态属性和静态方法,在 ES7
提案中,我们可以在变量或方法的前面加上 static
关键字来定义静态属性或静态方法。
静态属性
class Animal { static num = 42; constructor() { // ... } } console.log(Animal.num); // 42
1
2
3
4
5
6
7
8
9静态方法
class Animal { static isAnimal(a) { return a instanceof Animal; } } let a = new Animal('Jack'); Animal.isAnimal(a); // true a.isAnimal(a); // TypeError: a.isAnimal is not a function
1
2
3
4
5
6
7
8
9
# 6. 访问修饰符
TypeScript
可以使用三种访问修饰符(Access Modifiers
),分别是 public
、private
和 protected
。
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的;private
修饰的属性或方法是私有的,不能在声明它的类的外部访问;protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的;
# 6.1 public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public
来做修饰;例如,C# 要求必须明确地使用 public
指定成员是可见的。 在 TypeScript
里,成员都默认为 public
。
class Animal {
public name;
public constructor(name) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom
2
3
4
5
6
7
8
9
10
11
上面的例子中,name
被设置为了 public
,所以直接访问实例的 name
属性是允许的。
# 6.2 private
很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private
了,当成员被标记成 private
时,它就不能在声明它的类的外部访问。
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
2
3
4
5
6
7
8
9
10
11
12
13
需要注意的是,TypeScript
编译之后的代码中,并没有限制 private
属性在外部的可访问性。
上面的例子编译后的代码是:
var Animal = (function () {
function Animal(name) {
this.name = name;
}
return Animal;
})();
var a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';
2
3
4
5
6
7
8
9
使用 private
修饰的属性或方法,在子类中也是不允许访问的:
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name);
}
}
// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
而如果是用 protected
修饰,则允许在子类中访问。
# 6.3 protected
protected
修饰符与 private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问。例如:
class Animal {
public name: string;
public constructor(name: string) {
this.name = name;
}
public run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`);
}
}
class Person extends Animal {
private age: number = 18;
protected sex: string = "男";
run(distance: number = 5) {
console.log("Person jumping...");
super.run(distance);
}
}
class Student extends Person {
run(distance: number = 6) {
console.log("Student jumping...");
console.log(this.sex); // 子类能看到父类中受保护的成员
// console.log(this.age) // 子类看不到父类中私有的成员
super.run(distance);
}
}
console.log(new Person("abc").name); // 公开的可见
// console.log(new Person('abc').sex) // 受保护的不可见
// console.log(new Person('abc').age) // 私有的不可见
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
# 7. readonly 修饰符
你可以使用 readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Person {
readonly name: string = "abc";
constructor(name: string) {
this.name = name;
}
}
let john = new Person("John");
// john.name = 'peter' // error
2
3
4
5
6
7
8
9
# 8. 参数属性
在上面的例子中,我们必须在 Person
类里定义一个只读成员 name
和一个参数为 name
的构造函数,并且立刻将 name
的值赋给 this.name
,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person
类的修改版,使用了参数属性:
class Person2 {
constructor(readonly name: string) {}
}
const p = new Person2("jack");
console.log(p.name);
2
3
4
5
6
注意看我们是如何舍弃参数 name
,仅在构造函数里使用 readonly name: string
参数来创建和初始化 name
成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private
限定一个参数属性会声明并初始化一个私有成员;对于 public
和 protected
来说也是一样。
# 9. 抽象类
抽象类做为其它派生类的基类使用。abstract
关键字用于定义抽象类和在抽象类内部定义抽象方法。
什么是抽象类?
首先,抽象类是不允许被实例化的:
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
let a = new Animal('Jack');
// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.
2
3
4
5
6
7
8
9
10
11
上面的例子中,我们定义了一个抽象类 Animal
,并且定义了一个抽象方法 sayHi
。在实例化抽象类的时候报错了。
其次,抽象类中的抽象方法必须被子类实现:
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
class Cat extends Animal {
public eat() {
console.log(`${this.name} is eating.`);
}
}
let cat = new Cat('Tom');
// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面的例子中,我们定义了一个类 Cat
继承了抽象类 Animal
,但是没有实现抽象方法 sayHi
,所以编译报错了。
下面是一个正确使用抽象类的例子:
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`);
}
}
let cat = new Cat('Tom');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面的例子中,我们实现了抽象方法 sayHi
,编译通过了。