TypeScript

TypeScript

  • 开始
  • 官方文档
  • 语言规范

›手册

文档说明

  • 文档约定
  • 历史版本

入门指南

  • 五分钟速览

手册

  • 数据类型
  • 变量声明
  • 接口
  • 类
  • 函数
  • 泛型
  • 枚举
  • 类型推断
  • 类型兼容
  • 高级类型

类

简介

传统的 JavaScript 使用函数和面向原型的继承方式构建可重用组件,但这种方式可能让已经适应了面向对象编程的程序员感到有些别扭。面向对象编程中的类扩展其他类的功能,对象实例则通过类创建。从 ES2015 开始,也就是ES6,JavaScript 程序员也可以使用基于类的面向对象方式构建应用程序。在 TypeScript 中,我们现在已经可以让开发者使用这种技术,然后将代码编译成在各种浏览器和平台上都适用的 JavaScript 代码,不需要等待 JavaScript 的后续版本。

类

让我们先看一下这段简单的代码。

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

let greeter = new Greeter("world");

如果你使用过 C# 或者 Java,那你一定对这种语法比较熟悉。我们声明了一个新的类 Greeter,类有三个成员:greeting 属性,构造方法和 greet 方法。

在类的内部,当我们使用某个成员的时候在成员前面加 this.,这表明在使用类的成员方式进行访问。

在最后一行,我们使用 new 创建 Greeter 类的实例,创建时会调用构造方法初始化实例。

继承

在 TypeScript 中,我们可以使用面向对象编程的常见模式,而其中最基础的一个模式就是类的继承。

让我们看看这个例子。

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 是使用 extends 关键字从基类 Animal 扩展而来的派生类,派生类通常也叫做子类,基类通常又叫做父类。

因为 Dog 继承了 Animal 的功能,我们就可以创建一个既可以 bark() 又可以 move() 的 Dog的实例。

让我们看一个更复杂点的例子。

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

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

这个例子演示了之前不曾提到的特性,我们使用 extends 关键字创建了 Animal 的两个子类:Horse 和 Snake。

和以前的例子不同的是,每个派生类在构造方法中都通过 super() 调用了父类的构造方法。另外,当我们在构造方法体内部访问 this 属性之前都调用了 super(),这是 TypeScript 强制要求的一个规则。

这个例子也展示了如何使用子类方法覆盖父类的方法,Snake 和 Horse 都创建了 move 方法覆盖了 Animal 的 move 方法,同时为方法加入了更多的功能。注意虽然 tom 被声明为 Animal,但是它的 value 是 Horse,调用 tom.move(34) 实际调用的是 Horse 中重写的方法。

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

Public、 private 和 protected修饰符

Public 是默认值

在例子中,我们可以随意地访问我们在程序中声明的成员,如果你熟悉其他语言中的类,你可能注意到了我们上面的例子中并没有对成员使用 public 修饰符。例如,C# 要求成员必须被显式地声明为 public,在 TypeScript 中,每个成员默认就是 public。

你也可以显式地声明一个成员为 public,我们可以用下面的方式来重写之前的 Animal。

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 的时候,该成员只能从该类的内部访问,例如。

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

new Animal("Cat").name; // Error: 'name' is private;

TypeScript 是结构化的类型系统。当我们比较两种不同的类型时,无论它们来自何处,如果所有成员的类型都是兼容的,那么我们就可以说这两种类型本身是兼容的。

但是当类型包含 private 和 protected 成员时,处理方式就不一样了。假设两个类型是兼容的,如果其中一个有 private 成员,那么另外一个也必须有同源的 private 成员,对于 protected 成员来说也是一样。

我们来看看下面的例子。

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; // Error: 'Animal' and 'Employee' are not compatible

例子中有 Animal 和 Rhino 两个类,Rhino 是 Animal 的子类。例子中还有一个看上去结构和 Animal 一样的 Employee 类。我们创建这些类的实例,然后用这些实例彼此进行赋值,看看会发生什么。因为 Animal 和 Rhino 共有 private 声明,所以它们是兼容的。但是 Employee 并不是这样,当我们试图将 Employee 的实例赋值给由 Animal 声明的实例时,会返回类型不兼容的错误。虽然 Employee 也有一个 private 修饰的属性 name,但它不是我们在 Animal 中声明的那个 name。

Protected

protected 修饰符的行为与 private 很相似,仅仅只是有一点不同: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() {
        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); // error

注意在 Person 类的外部是无法访问 name 属性的,我们可以在 Employee 的实例方法中使用 name 属性,因为 Employee 是 Person 的子类。

构造方法也可以使用 protected 修饰符,这就是说不能在类的外部实例化类的实例,但是在子类中可以。例如。

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

// Employee can extend Person
class Employee extends Person {
    private department: string;

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

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is 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"; // error! name is readonly.

参数属性

在上面的例子中,我们在 Octopus 类中声明了只读成员 name 和带有 theName 参数的构造方法。这样我们在构造方法执行后仍然能 theName 的值。参数属性可以做到这一点,下面的例子使用参数属性对 Octopus 类进行进一步的改进。

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

现在我们移除了 theName 参数,同时在构造方法中使用 readonly name: string 方式的参数来初始化 name 成员。我们将声明和赋值合并到一起。

声明参数属性需要在构造方法上使用访问修饰符或者 readonly,或者二者一起使用。对参数属性使用 private 可以声明和初始化 private 成员,其他修饰符同理。

成员访问方法

TypeScript 支持对成员使用 getters/setters 方法,这使你可以更好地控制如何在每个对象上访问类的成员。

让我们看看使用 get 和 set 的一个简单例子。首先我们看看没有成员访问方法之前的代码。

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

有时允许使用者随意地设置 fullName 的值会很方便,但我们可能也想在对 fullName 设置值时加上一些限制。

在下面的版本中,我们加了 setter 方法,方法中会验证 newName 的长度以保证最大长度与后端数据库的字段长度兼容。如果不兼容,会在客户端提示错误。

为了保留现有功能,我们添加了简单的 getter 来获取未修改的 fullName 的值。

const fullNameMaxLength = 10;

class Employee {
    private _fullName: string;

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

    set fullName(newName: string) {
        if (newName && newName.length > fullNameMaxLength) {
            throw new Error("fullName has a max length of " + fullNameMaxLength);
        }

        this._fullName = newName;
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

为了证明我们的属性访问方法会检查输入值,我们可以给出一个超过 10 个字符的值。

有关成员访问方法的几点注意事项:

第一,要求你必须设置编译器的输出为 ES5 或者更高,ES3 是不支持的。第二,有 get 而没有 set 的成员将自动被识别为 readonly 成员。当通过代码生成 .d.ts 文件时这很有用。因为使用者无法对成员的值进行修改。

静态属性

到现在,我们都仅仅在讨论类的实例化成员,这些都是在类实例化时发生的。我们也可以为类创建静态成员,这些成员对类自身来说是可见的。在这个例子中,我们在 origin 上使用 static 修饰符。origin 是所有 grid 的属性。每个实例都通过类名加属性名访问静态属性,这看上去和前面加 this. 有点类似。在实例中访问可以使用 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 class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

抽象类中的抽象方法不能有任何实现,它必须在子类中实现。抽象方法和接口方法相似,二者都只定义方法的标记而没有任何方法体,但是抽象方法必须要使用 abstract 关键字,也可以包含访问修饰符。

abstract class Department {

    constructor(public name: string) {
    }

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

    abstract printMeeting(): void; // must be implemented in derived classes
}

class AccountingDepartment extends Department {

    constructor() {
        super("Accounting and Auditing"); // constructors in derived classes must call super()
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type

高级技巧

构造函数

当在 TypeScript 中声明类的时候,你实际上是在创建很多个声明,第一个声明就是实例的类型。

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

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 操作符时就会调用这个构造函数,我们会得到类的实例,构造函数也包含类的所有静态成员。

下面我们修改一下上面的例子。

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

在示例中,greeter1 的工作方式与之前类似。我们实例化 Greeter 类,并使用此对象,这是我们以前见过的。

接下来,我们创建一个名为 greeterMaker 的新的变量。这个变量将保存类本身,也就是用另一种方式表示其构造函数。在这里,我们使用 typeof Greeter,即“给我 Greeter 类本身的类型”,而不是实例类型。或者更确切地说,“给我称为 Greeter 的符号的类型”,这是构造函数的类型。该类型将包含 Greeter 的所有静态成员以及创建 Greeter 类实例的构造函数。我们通过在 greeterMaker 上使用 new,创建 Greeter 的新实例并像之前一样调用实例方法。

像接口那样使用类

之前我们说过,类定义包含两部分:表示类实例的类型以及构造函数。因为类创建类型,所以你可以像使用接口一样使用类。

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

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};
← 接口函数 →
  • 简介
  • 类
  • 继承
  • Public、 private 和 protected修饰符
    • Public 是默认值
    • Private
    • Protected
  • Readonly
    • 参数属性
  • 成员访问方法
  • 静态属性
  • 抽象类
  • 高级技巧
    • 构造函数
    • 像接口那样使用类
Copyright © 2025 unimits