TypeScript 初识 - 类

一个普通的类

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return 'Hello, ' + this.greeting;
    }
}

let greeter = new Greeter('world');

继承

super 作为函数调用时,代表父类的构造函数。子类的构造函数必须执行一次 super 函数,并且在构造函数里访问 this 的属性之前一定要调用 super()

class Animal {
    name: string;
    constructor(theName: string) {
        this.name = theName;
    }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) {
        super(name);
    }
    move(distanceInMeters = 5) {
        console.log('Slithering...');
        super.move(distanceInMeters);
    }
}

super 虽然代表了父类 Animal 的构造函数,但是返回的是子类 Snake 的实例,即 super 内部的 this 指的是 Snake 的实例,因此 super() 在这里相当于 Animal.prototype.constructor.call(this)

访问修饰符

public

在 TypeScript 中,所有访问修饰符默认为 public,这个和 JavaScript 是一致的:

// 这和上面例子是一致的
class Animal {
    public name: string;
    public constructor(theName: string) {
        this.name = theName;
    }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

private

private 意为私有,使用 private 修饰的变量,不允许在类的外面使用,子类中也不允许访问:

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

new Animal('Cat').name; // Error

在 TypeScript 中,如果两个类的所有成员的类型都是兼容的,这就表示两个类是兼容的。

但是,一个类里有 privateprotected 修饰的成员,另一个类中的变量必须拥有来自同一处声明相同修饰的变量,这两个类才算是兼容的:

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

class Rhino extends Animal {
    constructor() {
        super('Rhino');
    }
}

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

let animal = new Animal('Goat');
let rhino = new Rhino();
let employee = new Employee('Bob');

animal = rhino;
animal = employee; // 不能将类型“Employee”分配给类型“Animal”。类型具有私有属性“name”的单独声明。

protected

protected 唯一比 private 修饰的成员多出的权限在于,protected 修饰的成员可以在子类中使用,但仍不允许在类的外面使用。

构造函数可以被 protected 修饰,修饰后的类无法被实例化,只能被继承。

readonly 修饰符

readonly 可以将属性设置为只读,只读属性只能在声明时或者构造函数里进行初始化:

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor(theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus('Man with the 8 strong legs');
dad.name = 'Man with the 3-piece suit'; // 错误! name 是只读的

参数属性

参数属性可以方便地在一个类里定义并初始化一个成员:

// 这里和上面定义的类是一样的
class Octopus {
    readonly numberOfLegs: number = 8;
    // 把声明和赋值合并到一起
    constructor(readonly name: string) {}
}

参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private 限定一个参数属性会声明并初始化一个私有成员;publicprotected 也是一样。

存取器

TypeScript 中的类支持取值函数(getter)、存值函数(setter):

let passCode = 'secret passCode';

class Employee {
    // 私有属性
    private _fullName: string;

    // getter
    get fullName(): string {
        return this._fullName;
    }

    // setter
    set fullName(newName: string) {
        if (passCode && passCode == 'secret passCode') {
            this._fullName = newName;
        } else {
            console.log('Error: Unauthorized update of employee!');
        }
    }
}

let employee = new Employee();
employee.fullName = 'Bob Smith';
if (employee.fullName) {
    alert(employee.fullName);
}

存取器必须要编译器设置(compilerOptions.target)输出为 ES6 或更高。

只带有 get 不带有 set 的存取器会自动被推断为 readonly

静态属性

TypeScript 中的类支持静态成员,可以在没有实例化的情况下进行访问,使用 static 进行修饰:

class Grid {
    static origin = { x: 0, y: 0 };
    calculateDistanceFromOrigin(point: { x: number; y: number }) {
        // 类里使用静态成员需要加上类名,使用访问和 this 相同
        let xDist = point.x - Grid.origin.x;
        let yDist = point.y - Grid.origin.y;
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor(public scale: number) {}
}

let grid = new Grid(1.0);

console.log(grid.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 14.142135623730951
console.log(Grid.origin);
// Object {x: 0, y: 0}

静态方法调用不了实例化方法和实例化属性,因为静态域加载是在解析阶段,而实例化是在初始化阶段,所以静态方法里面不能调用本类的方法和属性,可以调用静态属性和静态方法。

抽象类

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

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('moving...');
    }
}

抽象类中的抽象方法不包含具体实现并且必须在字类中实现。抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

类和接口

类定义会创建两个东西:类的实例类型和一个构造函数。因为类可以创建出类型,所以可以在使用接口的地方使用类:

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

类可以实现(implement)接口。通过接口可以强制指明类遵守某个契约。也可以在接口中声明一个方法,然后要求类去具体实现它。

接口不可以被实例化,实现接口必须重写接口中的抽象方法。

类可以实现(implement)多个接口,但只能扩展(extends)自一个抽象类。

抽象类中可以包含具体实现,接口不能。

抽象类在运行时是可见的,可以通过 instanceof 判断。接口则只在编译时起作用。

接口只能描述类的公共(public)部分,不会检查私有成员,而抽象类没有这样的限制。