TypeScript 学习笔记(三)类

214 阅读3分钟

基本例子

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

let greeter = new Greeter("world");

我们声明了一个Greeter类,它包含三个成员:一个greeting属性,一个greet方法,一个构造函数。最后一行,我们使用new构造了Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。

继承

类允许继承,基本例子

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

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

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

在这个例子中,类从基类中继承了属性和方法,在这里,Dog是一个派生类,派生自Animal基类,派生类通常被称作子类,基类通常被称作超类。 复杂例子

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

class Horse extends Animal { 
    constructor(name: string) { 
      // 必须先调用super函数 
      super(name); 
    }
    // 重写父类的方法
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let tom: Animal = new Horse("Tommy the Palomino");
tom.move(34);

在这个例子中,子类中多了构造函数,当子类中包含构造函数时,必须 要先调用super(),它会执行超类的构造函数,并且一定要在构造函数访问this之前。子类还可以重写父类的方法。 所以,tom虽然被声明为animal类型,但它的值时House,所以执行的move方法是House中的move方法。

Galloping...
Tommy the Palomino moved 34m.

公共,私有与受保护的修饰符

public

默认为public,不需要特别去标记

private

私有属性不可以在类的外部被访问。

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

new Animal("Cat").name; // 错误: 'name' 是私有的.
protected

protected属性与私有属性比较相似,不同的是,protected可以在派生类中被访问,

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

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        // 派生类可以访问父类中的protected属性
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误,protected属性不可在类的外部访问。
readonly

只读属性,只读属性必须在声明时或构造函数里被初始化

参数属性
class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

将原来需要在构造函数声明之前定义的属性直接在构造函数里使用private name: string参数来创建和初始化name成员,把声明和赋值合并至一处。

存取器

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

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

    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);
}

首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly。

静态属性

静态属性是存在于类本身的成员,而不是在类的实例中,不需要在类的实例化时才被初始化。 访问静态属性,需要在属性名前加上类名,如同在实例属性上使用this.前缀来访问属性一样。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        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 grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类

抽象类一般是其他派生类的基类,一般不会实例化。 abstract 关键字主要是用于定义抽象类和抽象类中的方法,它的语法与接口的语法很相似,两者都是定义方法签名但不包含方法体。 不同于接口的是:抽象类中的抽象方法不包含具体实现但必须在派生类中实现;抽象类可以包含成员的实现细节;抽象方法必须包含abstract关键字并且可以包含访问修饰符。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    
    // 定义一个抽象基类中没有声明的方法,但是它没办法在实例中使用
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

高级类

构造函数

当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的实例的类型。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
// 声明greeter类的实例的类型是Greeter
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

这里,我们写了let greeter: Greeter,意思是Greeter类的实例的类型是Greeter。

我们也创建了一个叫做构造函数的值。 这个函数会在我们使用new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

上面的代码里,let Greeter将被赋值为构造函数。 当我们调用new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有实例部分与静态部分这两个部分。