传统的 JavaScript 使用函数和基于原型的继承来构建可重用的组件,但这对于更习惯于面向对象方法的程序员来说可能有点别扭,在面向对象方法中,类继承功能,对象从这些类构建。从 ECMAScript 2015 开始,也称为 ECMAScript 6,JavaScript 程序员可以使用这种面向对象的基于类的方案构建他们的应用程序。在 TypeScript 中,我们允许开发人员现在使用这些技术,并将它们编译成适用于所有主要浏览器和平台的 JavaScript,而无需等待下一个版本的 JavaScript。
类
让我们看一个简单的基于类的示例
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter = newGreeter ("world");
如果您以前使用过 C# 或 Java,语法应该很熟悉。我们声明一个新的类 Greeter
。此类具有三个成员:一个名为 greeting
的属性、一个构造函数和一个方法 greet
。
您会注意到,在类中,当我们引用类的成员之一时,我们在前面加上 this.
。这表示它是一个成员访问。
在最后一行,我们使用 new
构造 Greeter
类的实例。这会调用我们之前定义的构造函数,创建一个具有 Greeter
形状的新对象,并运行构造函数来初始化它。
继承
在 TypeScript 中,我们可以使用常见的面向对象模式。基于类的编程中最基本的一种模式是能够扩展现有类以使用继承创建新的类。
让我们看一个例子
tsTry
classAnimal {move (distanceInMeters : number = 0) {console .log (`Animal moved ${distanceInMeters }m.`);}}classDog extendsAnimal {bark () {console .log ("Woof! Woof!");}}constdog = newDog ();dog .bark ();dog .move (10);dog .bark ();
此示例展示了最基本的继承特性:类从基类继承属性和方法。这里,Dog
是一个派生类,它使用 extends
关键字从 Animal
基类派生。派生类通常称为子类,基类通常称为超类。
因为 Dog
扩展了 Animal
的功能,所以我们能够创建一个 Dog
实例,它既可以 bark()
又可以 move()
。
现在让我们来看一个更复杂的例子。
tsTry
classAnimal {name : string;constructor(theName : string) {this.name =theName ;}move (distanceInMeters : number = 0) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}classSnake extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 5) {console .log ("Slithering...");super.move (distanceInMeters );}}classHorse extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 45) {console .log ("Galloping...");super.move (distanceInMeters );}}letsam = newSnake ("Sammy the Python");lettom :Animal = newHorse ("Tommy the Palomino");sam .move ();tom .move (34);
此示例涵盖了我们之前没有提到的其他一些特性。同样,我们看到 extends
关键字用于创建 Animal
的两个新子类:Horse
和 Snake
。
与之前示例的不同之处在于,每个包含构造函数的派生类必须调用 super()
,这将执行基类的构造函数。更重要的是,在我们任何访问构造函数体中 this
上的属性之前,我们必须调用 super()
。这是一个重要的规则,TypeScript 将强制执行。
此示例还展示了如何用专门用于子类的方法覆盖基类中的方法。这里,Snake
和 Horse
都创建了一个 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
类
tsTry
classAnimal {publicname : string;public constructor(theName : string) {this.name =theName ;}publicmove (distanceInMeters : number) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}
ECMAScript 私有字段
使用 TypeScript 3.8,TypeScript 支持 JavaScript 中用于私有字段的新语法
tsTry
classAnimal {#name: string;constructor(theName : string) {this.#name =theName ;}}newProperty '#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.Animal ("Cat").#name ;
此语法内置于 JavaScript 运行时,并且可以更好地保证每个私有字段的隔离。目前,这些私有字段的最佳文档在 TypeScript 3.8 发行说明中。
理解 TypeScript 的 private
TypeScript 也拥有自己的方式来声明一个成员为 private
,它不能从其包含的类之外访问。例如
tsTry
classAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}newProperty 'name' is private and only accessible within class 'Animal'.2341Property 'name' is private and only accessible within class 'Animal'.Animal ("Cat").; name
TypeScript 是一个结构化类型系统。当我们比较两种不同的类型时,无论它们来自哪里,如果所有成员的类型都兼容,那么我们就说类型本身是兼容的。
但是,当比较具有 private
和 protected
成员的类型时,我们对这些类型的处理方式不同。为了使两种类型被认为是兼容的,如果其中一个类型具有 private
成员,那么另一个类型必须具有一个源自相同声明的 private
成员。protected
成员也是如此。
让我们看一个例子,以便更好地了解这在实践中是如何发挥作用的
tsTry
classAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}classRhino extendsAnimal {constructor() {super("Rhino");}}classEmployee {privatename : string;constructor(theName : string) {this.name =theName ;}}letanimal = newAnimal ("Goat");letrhino = newRhino ();letemployee = newEmployee ("Bob");animal =rhino ;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'.= animal employee ;
在这个例子中,我们有一个 Animal
和一个 Rhino
,Rhino
是 Animal
的子类。我们还有一个新的类 Employee
,它在形状上与 Animal
相同。我们创建了这些类的实例,然后尝试将它们相互赋值,看看会发生什么。由于 Animal
和 Rhino
从 Animal
中 private name: string
的相同声明共享了 private
方面的形状,因此它们是兼容的。但是,对于 Employee
来说情况并非如此。当我们尝试从 Employee
赋值到 Animal
时,我们得到一个错误,表明这些类型不兼容。即使 Employee
也具有一个名为 name
的 private
成员,但它不是我们在 Animal
中声明的。
理解 protected
protected
修饰符的作用与 private
修饰符非常相似,区别在于声明为 protected
的成员也可以在派生类中访问。例如,
tsTry
classPerson {protectedname : string;constructor(name : string) {this.name =name ;}}classEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");console .log (howard .getElevatorPitch ());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.console .log (howard .); name
请注意,虽然我们无法从 `Person` 类外部使用 `name`,但我们仍然可以在 `Employee` 的实例方法中使用它,因为 `Employee` 派生自 `Person`。
构造函数也可以标记为 `protected`。这意味着该类不能在其包含类之外实例化,但可以扩展。例如,
tsTry
classPerson {protectedname : string;protected constructor(theName : string) {this.name =theName ;}}// Employee can extend PersonclassEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");letConstructor 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.john = newPerson ("John");
只读修饰符
您可以使用 `readonly` 关键字使属性只读。只读属性必须在声明时或在构造函数中初始化。
tsTry
classOctopus {readonlyname : string;readonlynumberOfLegs : number = 8;constructor(theName : string) {this.name =theName ;}}letdad = newOctopus ("Man with the 8 strong legs");Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.dad .= "Man with the 3-piece suit"; name
参数属性
在我们最后一个例子中,我们必须在 `Octopus` 类中声明一个只读成员 `name` 和一个构造函数参数 `theName`。这是为了在 `Octopus` 构造函数执行后使 `theName` 的值可访问。参数属性允许您在一个地方创建和初始化一个成员。以下是使用参数属性对之前 `Octopus` 类的进一步修改
tsTry
classOctopus {readonlynumberOfLegs : number = 8;constructor(readonlyname : string) {}}letdad = newOctopus ("Man with the 8 strong legs");dad .name ;
请注意,我们完全删除了 `theName`,只使用构造函数上的缩写 `readonly name: string` 参数来创建和初始化 `name` 成员。我们已将声明和赋值合并到一个位置。
参数属性通过在构造函数参数前添加访问修饰符或 `readonly`,或两者来声明。使用 `private` 声明参数属性会声明和初始化一个私有成员;同样,`public`、`protected` 和 `readonly` 也是如此。
访问器
TypeScript 支持使用 getter/setter 来拦截对对象成员的访问。这为您提供了一种更细粒度地控制每个对象上成员访问方式的方法。
让我们将一个简单的类转换为使用 get
和 set
。首先,让我们从一个没有 getter 和 setter 的示例开始。
tsTry
classEmployee {fullName : string;}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
虽然允许人们直接随机设置 fullName
很方便,但我们可能也希望在设置 fullName
时强制执行一些约束。
在这个版本中,我们添加了一个 setter,它检查 newName
的长度,以确保它与我们后端数据库字段的最大长度兼容。如果不是,我们会抛出一个错误,通知客户端代码发生了错误。
为了保留现有功能,我们还添加了一个简单的 getter,它以未修改的方式检索 fullName
。
tsTry
constfullNameMaxLength = 10;classEmployee {private_fullName : string = "";getfullName (): string {return this._fullName ;}setfullName (newName : string) {if (newName &&newName .length >fullNameMaxLength ) {throw newError ("fullName has a max length of " +fullNameMaxLength );}this._fullName =newName ;}}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
为了证明我们的访问器现在正在检查值的长度,我们可以尝试分配一个超过 10 个字符的名称,并验证我们是否收到错误。
关于访问器需要注意的几点
首先,访问器要求您将编译器设置为输出 ECMAScript 5 或更高版本。降级到 ECMAScript 3 不受支持。其次,具有 get
但没有 set
的访问器会自动推断为 readonly
。这在从代码生成 .d.ts
文件时很有用,因为您的属性的用户可以看到他们无法更改它。
静态属性
到目前为止,我们只讨论了类的实例成员,这些成员在实例化时会出现在对象上。我们还可以创建类的静态成员,这些成员在类本身而不是实例上可见。在这个例子中,我们在原点上使用 static
,因为它对所有网格都是一个通用值。每个实例都通过在类名前面加上名称来访问此值。类似于在实例访问之前加上 this.
,这里我们在静态访问之前加上 Grid.
。
tsTry
classGrid {staticorigin = {x : 0,y : 0 };calculateDistanceFromOrigin (point : {x : number;y : number }) {letxDist =point .x -Grid .origin .x ;letyDist =point .y -Grid .origin .y ;returnMath .sqrt (xDist *xDist +yDist *yDist ) / this.scale ;}constructor(publicscale : number) {}}letgrid1 = newGrid (1.0); // 1x scaleletgrid2 = newGrid (5.0); // 5x scaleconsole .log (grid1 .calculateDistanceFromOrigin ({x : 10,y : 10 }));console .log (grid2 .calculateDistanceFromOrigin ({x : 10,y : 10 }));
抽象类
抽象类是基类,其他类可以从它派生。它们不能直接实例化。与接口不同,抽象类可以包含其成员的实现细节。abstract
关键字用于定义抽象类以及抽象类中的抽象方法。
tsTry
abstract classAnimal {abstractmakeSound (): void;move (): void {console .log ("roaming the earth...");}}
抽象类中标记为抽象的方法不包含实现,必须在派生类中实现。抽象方法与接口方法的语法类似。两者都定义了方法的签名,但不包括方法体。但是,抽象方法必须包含 abstract
关键字,并且可以选择包含访问修饰符。
tsTry
abstract classDepartment {constructor(publicname : string) {}printName (): void {console .log ("Department name: " + this.name );}abstractprintMeeting (): void; // must be implemented in derived classes}classAccountingDepartment extendsDepartment {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...");}}letdepartment :Department ; // ok to create a reference to an abstract typeCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.department = newDepartment (); // error: cannot create an instance of an abstract classdepartment = newAccountingDepartment (); // ok to create and assign a non-abstract subclassdepartment .printName ();department .printMeeting ();Property 'generateReports' does not exist on type 'Department'.2339Property 'generateReports' does not exist on type 'Department'.department .(); // error: department is not of type AccountingDepartment, cannot access generateReports generateReports
高级技巧
构造函数
在 TypeScript 中声明类时,实际上是在同时创建多个声明。第一个是类实例的类型。
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter :Greeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
在这里,当我们说 let greeter: Greeter
时,我们使用 Greeter
作为类 Greeter
实例的类型。对于来自其他面向对象语言的程序员来说,这几乎是第二天性。
我们还创建了另一个称为构造函数的值。这是在使用new
创建类实例时调用的函数。为了实际了解它,让我们看一下上面示例生成的 JavaScript 代码。
tsTry
letGreeter = (function () {functionGreeter (message ) {this.greeting =message ;}Greeter .prototype .greet = function () {return "Hello, " + this.greeting ;};returnGreeter ;})();letgreeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
在这里,let Greeter
将被赋值为构造函数。当我们调用new
并运行此函数时,我们将获得类的实例。构造函数还包含类中的所有静态成员。另一种思考每个类的方式是,它有一个实例侧和一个静态侧。
让我们修改一下示例以显示这种差异。
tsTry
classGreeter {staticstandardGreeting = "Hello, there";greeting : string;greet () {if (this.greeting ) {return "Hello, " + this.greeting ;} else {returnGreeter .standardGreeting ;}}}letgreeter1 :Greeter ;greeter1 = newGreeter ();console .log (greeter1 .greet ()); // "Hello, there"letgreeterMaker : typeofGreeter =Greeter ;greeterMaker .standardGreeting = "Hey there!";letgreeter2 :Greeter = newgreeterMaker ();console .log (greeter2 .greet ()); // "Hey there!"letgreeter3 :Greeter ;greeter3 = newGreeter ();console .log (greeter3 .greet ()); // "Hey there!"
在这个示例中,greeter1
的工作方式与之前类似。我们实例化了Greeter
类,并使用此对象。我们之前已经见过这种情况。
接下来,我们直接使用类。在这里,我们创建一个名为greeterMaker
的新变量。此变量将保存类本身,或者换句话说,它的构造函数。在这里,我们使用typeof Greeter
,即“给我Greeter
类本身的类型”,而不是实例类型。或者更准确地说,“给我名为Greeter
的符号的类型”,即构造函数的类型。此类型将包含 Greeter 的所有静态成员以及用于创建Greeter
类实例的构造函数。我们通过在greeterMaker
上使用new
来展示这一点,创建Greeter
的新实例并像以前一样调用它们。还需要提一下,更改静态属性的做法是不好的,这里greeter3
的standardGreeting
上是"Hey there!"
而不是"Hello, there"
。
使用类作为接口
正如我们在上一节中所说,类声明创建了两个东西:一个表示类实例的类型和一个构造函数。由于类创建类型,因此您可以在使用接口的地方使用它们。
tsTry
classPoint {x : number;y : number;}interfacePoint3d extendsPoint {z : number;}letpoint3d :Point3d = {x : 1,y : 2,z : 3 };