此页面已弃用

此手册页面已被替换,转到新页面

传统的 JavaScript 使用函数和基于原型的继承来构建可重用的组件,但这对于更习惯于面向对象方法的程序员来说可能有点别扭,在面向对象方法中,类继承功能,对象从这些类构建。从 ECMAScript 2015 开始,也称为 ECMAScript 6,JavaScript 程序员可以使用这种面向对象的基于类的方案构建他们的应用程序。在 TypeScript 中,我们允许开发人员现在使用这些技术,并将它们编译成适用于所有主要浏览器和平台的 JavaScript,而无需等待下一个版本的 JavaScript。

让我们看一个简单的基于类的示例

ts
class Greeter {
greeting: string;
 
constructor(message: string) {
this.greeting = message;
}
 
greet() {
return "Hello, " + this.greeting;
}
}
 
let greeter = new Greeter("world");
Try

如果您以前使用过 C# 或 Java,语法应该很熟悉。我们声明一个新的类 Greeter。此类具有三个成员:一个名为 greeting 的属性、一个构造函数和一个方法 greet

您会注意到,在类中,当我们引用类的成员之一时,我们在前面加上 this.。这表示它是一个成员访问。

在最后一行,我们使用 new 构造 Greeter 类的实例。这会调用我们之前定义的构造函数,创建一个具有 Greeter 形状的新对象,并运行构造函数来初始化它。

继承

在 TypeScript 中,我们可以使用常见的面向对象模式。基于类的编程中最基本的一种模式是能够扩展现有类以使用继承创建新的类。

让我们看一个例子

ts
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();
Try

此示例展示了最基本的继承特性:类从基类继承属性和方法。这里,Dog 是一个派生类,它使用 extends 关键字从 Animal 类派生。派生类通常称为子类,基类通常称为超类

因为 Dog 扩展了 Animal 的功能,所以我们能够创建一个 Dog 实例,它既可以 bark() 又可以 move()

现在让我们来看一个更复杂的例子。

ts
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);
Try

此示例涵盖了我们之前没有提到的其他一些特性。同样,我们看到 extends 关键字用于创建 Animal 的两个新子类:HorseSnake

与之前示例的不同之处在于,每个包含构造函数的派生类必须调用 super(),这将执行基类的构造函数。更重要的是,在我们任何访问构造函数体中 this 上的属性之前,我们必须调用 super()。这是一个重要的规则,TypeScript 将强制执行。

此示例还展示了如何用专门用于子类的方法覆盖基类中的方法。这里,SnakeHorse 都创建了一个 move 方法,它覆盖了 Animal 中的 move,赋予它特定于每个类的功能。请注意,即使 tom 被声明为 Animal,由于它的值是 Horse,所以调用 tom.move(34) 将调用 Horse 中的覆盖方法。

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

公共、私有和受保护的修饰符

默认公开

在我们的示例中,我们能够自由地访问在整个程序中声明的成员。如果您熟悉其他语言中的类,您可能已经注意到在上面的示例中,我们不必使用public来实现这一点;例如,C# 要求每个成员都明确标记为public才能可见。在 TypeScript 中,每个成员默认都是public

您仍然可以明确地将成员标记为public。我们可以用以下方式编写上一节中的Animal

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

ECMAScript 私有字段

使用 TypeScript 3.8,TypeScript 支持 JavaScript 中用于私有字段的新语法

ts
class Animal {
#name: string;
constructor(theName: string) {
this.#name = theName;
}
}
 
new Animal("Cat").#name;
Property '#name' is not accessible outside class 'Animal' because it has a private identifier.18013Property '#name' is not accessible outside class 'Animal' because it has a private identifier.
Try

此语法内置于 JavaScript 运行时,并且可以更好地保证每个私有字段的隔离。目前,这些私有字段的最佳文档在 TypeScript 3.8 发行说明中。

理解 TypeScript 的 private

TypeScript 也拥有自己的方式来声明一个成员为 private,它不能从其包含的类之外访问。例如

ts
class Animal {
private name: string;
 
constructor(theName: string) {
this.name = theName;
}
}
 
new Animal("Cat").name;
Property 'name' is private and only accessible within class 'Animal'.2341Property 'name' is private and only accessible within class 'Animal'.
Try

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

但是,当比较具有 privateprotected 成员的类型时,我们对这些类型的处理方式不同。为了使两种类型被认为是兼容的,如果其中一个类型具有 private 成员,那么另一个类型必须具有一个源自相同声明的 private 成员。protected 成员也是如此。

让我们看一个例子,以便更好地了解这在实践中是如何发挥作用的

ts
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;
Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.2322Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.
Try

在这个例子中,我们有一个 Animal 和一个 RhinoRhinoAnimal 的子类。我们还有一个新的类 Employee,它在形状上与 Animal 相同。我们创建了这些类的实例,然后尝试将它们相互赋值,看看会发生什么。由于 AnimalRhinoAnimalprivate name: string 的相同声明共享了 private 方面的形状,因此它们是兼容的。但是,对于 Employee 来说情况并非如此。当我们尝试从 Employee 赋值到 Animal 时,我们得到一个错误,表明这些类型不兼容。即使 Employee 也具有一个名为 nameprivate 成员,但它不是我们在 Animal 中声明的。

理解 protected

protected 修饰符的作用与 private 修饰符非常相似,区别在于声明为 protected 的成员也可以在派生类中访问。例如,

ts
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);
Property 'name' is protected and only accessible within class 'Person' and its subclasses.2445Property 'name' is protected and only accessible within class 'Person' and its subclasses.
Try

请注意,虽然我们无法从 `Person` 类外部使用 `name`,但我们仍然可以在 `Employee` 的实例方法中使用它,因为 `Employee` 派生自 `Person`。

构造函数也可以标记为 `protected`。这意味着该类不能在其包含类之外实例化,但可以扩展。例如,

ts
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");
Constructor of class 'Person' is protected and only accessible within the class declaration.2674Constructor of class 'Person' is protected and only accessible within the class declaration.
Try

只读修饰符

您可以使用 `readonly` 关键字使属性只读。只读属性必须在声明时或在构造函数中初始化。

ts
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";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
Try

参数属性

在我们最后一个例子中,我们必须在 `Octopus` 类中声明一个只读成员 `name` 和一个构造函数参数 `theName`。这是为了在 `Octopus` 构造函数执行后使 `theName` 的值可访问。参数属性允许您在一个地方创建和初始化一个成员。以下是使用参数属性对之前 `Octopus` 类的进一步修改

ts
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}
 
let dad = new Octopus("Man with the 8 strong legs");
dad.name;
Try

请注意,我们完全删除了 `theName`,只使用构造函数上的缩写 `readonly name: string` 参数来创建和初始化 `name` 成员。我们已将声明和赋值合并到一个位置。

参数属性通过在构造函数参数前添加访问修饰符或 `readonly`,或两者来声明。使用 `private` 声明参数属性会声明和初始化一个私有成员;同样,`public`、`protected` 和 `readonly` 也是如此。

访问器

TypeScript 支持使用 getter/setter 来拦截对对象成员的访问。这为您提供了一种更细粒度地控制每个对象上成员访问方式的方法。

让我们将一个简单的类转换为使用 getset。首先,让我们从一个没有 getter 和 setter 的示例开始。

ts
class Employee {
fullName: string;
}
 
let employee = new Employee();
employee.fullName = "Bob Smith";
 
if (employee.fullName) {
console.log(employee.fullName);
}
Try

虽然允许人们直接随机设置 fullName 很方便,但我们可能也希望在设置 fullName 时强制执行一些约束。

在这个版本中,我们添加了一个 setter,它检查 newName 的长度,以确保它与我们后端数据库字段的最大长度兼容。如果不是,我们会抛出一个错误,通知客户端代码发生了错误。

为了保留现有功能,我们还添加了一个简单的 getter,它以未修改的方式检索 fullName

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

为了证明我们的访问器现在正在检查值的长度,我们可以尝试分配一个超过 10 个字符的名称,并验证我们是否收到错误。

关于访问器需要注意的几点

首先,访问器要求您将编译器设置为输出 ECMAScript 5 或更高版本。降级到 ECMAScript 3 不受支持。其次,具有 get 但没有 set 的访问器会自动推断为 readonly。这在从代码生成 .d.ts 文件时很有用,因为您的属性的用户可以看到他们无法更改它。

静态属性

到目前为止,我们只讨论了类的实例成员,这些成员在实例化时会出现在对象上。我们还可以创建类的静态成员,这些成员在类本身而不是实例上可见。在这个例子中,我们在原点上使用 static,因为它对所有网格都是一个通用值。每个实例都通过在类名前面加上名称来访问此值。类似于在实例访问之前加上 this.,这里我们在静态访问之前加上 Grid.

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

抽象类

抽象类是基类,其他类可以从它派生。它们不能直接实例化。与接口不同,抽象类可以包含其成员的实现细节。abstract 关键字用于定义抽象类以及抽象类中的抽象方法。

ts
abstract class Animal {
abstract makeSound(): void;
 
move(): void {
console.log("roaming the earth...");
}
}
Try

抽象类中标记为抽象的方法不包含实现,必须在派生类中实现。抽象方法与接口方法的语法类似。两者都定义了方法的签名,但不包括方法体。但是,抽象方法必须包含 abstract 关键字,并且可以选择包含访问修饰符。

ts
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
Cannot create an instance of an abstract class.2511Cannot 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: department is not of type AccountingDepartment, cannot access generateReports
Property 'generateReports' does not exist on type 'Department'.2339Property 'generateReports' does not exist on type 'Department'.
Try

高级技巧

构造函数

在 TypeScript 中声明类时,实际上是在同时创建多个声明。第一个是类实例的类型。

ts
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()); // "Hello, world"
Try

在这里,当我们说 let greeter: Greeter 时,我们使用 Greeter 作为类 Greeter 实例的类型。对于来自其他面向对象语言的程序员来说,这几乎是第二天性。

我们还创建了另一个称为构造函数的值。这是在使用new创建类实例时调用的函数。为了实际了解它,让我们看一下上面示例生成的 JavaScript 代码。

ts
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()); // "Hello, world"
Try

在这里,let Greeter 将被赋值为构造函数。当我们调用new并运行此函数时,我们将获得类的实例。构造函数还包含类中的所有静态成员。另一种思考每个类的方式是,它有一个实例侧和一个静态侧。

让我们修改一下示例以显示这种差异。

ts
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()); // "Hello, there"
 
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
 
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // "Hey there!"
 
let greeter3: Greeter;
greeter3 = new Greeter();
console.log(greeter3.greet()); // "Hey there!"
Try

在这个示例中,greeter1 的工作方式与之前类似。我们实例化了Greeter 类,并使用此对象。我们之前已经见过这种情况。

接下来,我们直接使用类。在这里,我们创建一个名为greeterMaker 的新变量。此变量将保存类本身,或者换句话说,它的构造函数。在这里,我们使用typeof Greeter,即“给我Greeter 类本身的类型”,而不是实例类型。或者更准确地说,“给我名为Greeter 的符号的类型”,即构造函数的类型。此类型将包含 Greeter 的所有静态成员以及用于创建Greeter 类实例的构造函数。我们通过在greeterMaker 上使用new 来展示这一点,创建Greeter 的新实例并像以前一样调用它们。还需要提一下,更改静态属性的做法是不好的,这里greeter3standardGreeting 上是"Hey there!" 而不是"Hello, there"

使用类作为接口

正如我们在上一节中所说,类声明创建了两个东西:一个表示类实例的类型和一个构造函数。由于类创建类型,因此您可以在使用接口的地方使用它们。

ts
class Point {
x: number;
y: number;
}
 
interface Point3d extends Point {
z: number;
}
 
let point3d: Point3d = { x: 1, y: 2, z: 3 };
Try

TypeScript 文档是一个开源项目。欢迎您通过 发送 Pull Request ❤ 来帮助我们改进这些页面。

本页贡献者
RCRyan Cavanaugh (53)
DRDaniel Rosenwasser (27)
OTOrta Therox (21)
NSNathan Shively-Sanders (8)
BWBrice Wilson (5)
25+

上次更新时间:2024 年 3 月 21 日