0%

TypeScript class类

JavaScript使用函数和基于原型的继承来创建可重用的组件,这和常见的一些面向对象语言比如PHP、C#等来讲是有很大差别的,从ES6开始,就可以在js中使用基于类的面向对象的方式。TypeScript提供了类这个特性。

使用关键字class来创建一个类:

1
2
3
4
5
6
7
8
9
10
11
12

class Hero {
name: string;
constructor(name: string) {
this.name = name;
}

print() {
console.log('hero name is :', this.name);
}
}
let hero = new Hero('Iron man');

我们声明的这个类具有三个成员:一个name属性,一个构造方法contructor()和一个print()。在调用类成员的时候使用this。最后我们使用new实例化了一个Hero类的hero对象。并使用构造函数初始化它。

继承

基于类的程序设计中一种最基本的模式是允许使用继承来扩展先有的类。使用关键字extends来继承一个类。用来继承的类称为基类,继承后的类称为派生类。派生类通常称为子类,基类通常称为超类。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

class Dog extends Animal {
bark() {
console.log('Woof! woof!');
}
}

const dog = new Dog();

我们声明了一个基类Animal,拥有一个成员:方法move()。然后派生一个子类Dog,子类Dog同时拥有自己的方法bark和父类的方法move

当然,如果子类Dog也实现了父类的方法move,那么对于这个子类来讲,父类的move方法会被重写。也就是子类调用的是自己的move方法,而不是父类的。

类成员修饰符

类的成员默认是公共的,也就是说子类继承了父类后,子类是默认拥有父类的所有特性的,但是我们需要的并不是全部继承,有些父类的属性是需要保持私有的不被继承,有些属性是需要被保护的。我们可以用类成员修饰符来管理父类的成员权限。

public 公共的

在TypeScript里面,类成员默认是公共的。也可以给公共成员特意加上public修饰符。如果类成员是公共的,那我们可以自由使用这个成员。重写上面的Animal类:

1
2
3
4
5
class Animal {
public move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

private 私有的

当类的一个成员被标记为private的时候,是无法在类的外部使用该成员的,包括继承该类的派生类也无法访问。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
private name: string;
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

class Dog extends Animal {
bark() {
console.log(this.name); // error: 没有name属性
console.log('Woof! woof!');
}
}

当我们将基类的name属性设置为私有属性时,子类无法访问该属性。

TypeScript是结构类型系统,当我们比较两种不同类型时,并不在乎他们是从哪里来,而在于所有的结构是否兼容,如果所有成员类型都是兼容的,那么我们认为这两个类型是兼容的。

但是,当我们比较两个带有privateprotected成员的类型的时候,如果一个类型里面包含一个private成员,那么只有当另外一个类型也存在这样一个private成员,并且他们都是来自同一处声明时,才认为两个类型是兼容的,对protected成员也适用。

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 Animal {
private name: string;
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

class Dog extends Animal {
bark() {
console.log('Woof! woof!');
}
}

class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}

let employee = new Employee('test');
let animal = new Animal();
let dog = new Dog();
animal = employee // error: 不同类型,无法赋值
animal = dog; // ok

上面虽然相对于类Aniaml定义了一个类似的类Employee,但是两个是不同的类型,无法赋值,因为有私有属性name。而类Dog的实例dog是可以给animal赋值的,因为Dog继承了Animal,所以共享了Animal的私有成员name,因此他们是兼容的,

protected 受保护的

protected修饰的类成员,无法在类外部使用,但是可以在子类中访问。

例如,我们将上面Animal类的name属性修改为protected

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
protected name: string;
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

class Dog extends Animal {
bark() {
console.log(this.name);
console.log('Woof! woof!');
}
}

构造函数也可以被修饰为protected,这意味着这个类不能实例化,但是能被继承。

还是使用上面的两个类,我们把类Animal的构造函数标记为protected

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
protected name: string;
protected constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}

class Dog extends Animal {
constructor(name: string) {
super(name);
}
bark() {
console.log(this.name);
console.log('Woof! woof!');
}
}
let animal = new Animal('旺财'); // error 无法实例化
let dog = new Dog('旺财');

readonly 只读

可以使用readony修饰符将类成员声明为只读的,只读属性必须在声明时或在构造函数里被初始化。

例如,我们还是使用Dog这个类,声明一个leg属性:

1
2
3
4
5
6
7
8
9
10
class Dog extends Animal {
private readonly leg: number = 4;
constructor(name: string) {
super(name);
}
bark() {
console.log(this.name);
console.log('Woof! woof!');
}
}

上面使用了privatereadony两个修饰符声明了leg这个属性不仅是只读的,而且是私有的。

参数属性

有时候我们在定义类的时候,会定义一些私有属性,然后在构造函数中根据参数立马赋值。我们可以使用参数属性来在一个订房定义并初始化一个成员,例如改写上面的Animal类:

1
2
3
4
5
6
7
8
9
class Animal {
constructor(private name: string) {
console.log(this.name);
}
move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}
let aniaml = new Animal('旺财');

这样,在构造函数使用private name: string参数来声明并初始化name成员。

存取器

TypeScript支持通过getters/setters来控制对对象成员的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal {
private _name: string;
constructor(name) {
this.name = name;
}

get name(): string {
return this._name;
}

set name(newName: string) {
if (newName) {
this.name = newName;
} else {
console.log('name cant null');
}
}

move(distanceInMeters: number = 0) {
console.log('animal moved ' + distanceInMeters);
}
}
let aniaml = new Animal('');

这里我们给_name属性定义了存取器name,给_name加了一层保护。

需要注意的是,当只有get不带set的存取器自动被推断为readonly。以为只有get意味着不期望改变它的值。

静态属性

上面的属性都是实例成员,当类被实例化为对象时才可以初始化并访问的属性,我们也可以创建类的静态成员,这些静态成员存在于类本身上而不是实例上。也就是说,每个对象都有自己的实例成员,而静态成员是不受实例对象影响的。

例如,我们给类Animal加上统计其实例化对象数量的静态属性animal_number

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
static animal_number = 0;
private name: string;
constructor(name) {
Animal.animal_number ++;
this.name = name;
}
}
let aniaml1 = new Animal('');
console.log(Animal.animal_number); // 1
let aniaml2 = new Animal('dog');
console.log(Animal.animal_number); // 2

抽象类

抽象类作为其他派生类的基类使用,一般不会直接实例化。不同于接口,抽象类可以包含成员的实现细节。abstract关键字是定义抽象类和在抽象类内部定义抽象方法的修饰符。

例如,我们把类Animal定义为抽象类:

1
2
3
4
5
6
7
abstract class Animal {
private name: string;
constructor(name) {
this.name = name;
}
abstract move(): void;
}

这样,Animal类无法直接实例化,抽象类中的抽象方法不包含具体实现,但是必须在派生类中实现。

1
2
3
4
5
6
7
8
9

class Dog extends Animal {
move(): void {
console.log('四条腿走路');
}
}

let dog = new Dog('旺财');
dog.move();

构造函数

构造函数会在我们使用new创建类实例的时候调用。在派生类中使用父类的构造函数,使用supre(params)来调用。

码字辛苦,打赏个咖啡☕️可好?💘