背景阅读:
类(MDN)
TypeScript 完全支持 ES2015 中引入的 class
关键字。
¥TypeScript offers full support for the class
keyword introduced in ES2015.
与其他 JavaScript 语言功能一样,TypeScript 添加了类型注释和其他语法,以允许你表达类和其他类型之间的关系。
¥As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.
类成员
¥Class Members
这是最基本的类 - 一个空的:
¥Here’s the most basic class - an empty one:
tsTry
classPoint {}
这个类还不是很有用,所以让我们开始添加一些成员。
¥This class isn’t very useful yet, so let’s start adding some members.
字段
¥Fields
字段声明在类上创建公共可写属性:
¥A field declaration creates a public writeable property on a class:
tsTry
classPoint {x : number;y : number;}constpt = newPoint ();pt .x = 0;pt .y = 0;
与其他位置一样,类型注释是可选的,但如果未指定,则为隐式 any
。
¥As with other locations, the type annotation is optional, but will be an implicit any
if not specified.
字段也可以有初始化器;这些将在实例化类时自动运行:
¥Fields can also have initializers; these will run automatically when the class is instantiated:
tsTry
classPoint {x = 0;y = 0;}constpt = newPoint ();// Prints 0, 0console .log (`${pt .x }, ${pt .y }`);
就像 const
、let
和 var
一样,类属性的初始化器将用于推断其类型:
¥Just like with const
, let
, and var
, the initializer of a class property will be used to infer its type:
tsTry
constpt = newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x = "0";
--strictPropertyInitialization
strictPropertyInitialization
设置控制类字段是否需要在构造函数中初始化。
¥The strictPropertyInitialization
setting controls whether class fields need to be initialized in the constructor.
tsTry
classBadGreeter {Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.: string; name }
tsTry
classGoodGreeter {name : string;constructor() {this.name = "hello";}}
请注意,该字段需要在构造函数本身中进行初始化。TypeScript 不会分析你从构造函数调用的方法来检测初始化,因为派生类可能会覆盖这些方法并且无法初始化成员。
¥Note that the field needs to be initialized in the constructor itself. TypeScript does not analyze methods you invoke from the constructor to detect initializations, because a derived class might override those methods and fail to initialize the members.
如果你打算通过构造函数以外的方式明确地初始化一个字段(例如,可能一个外部库正在为你填充你的类的一部分),你可以使用明确的赋值断言运算符,!
:
¥If you intend to definitely initialize a field through means other than the constructor (for example, maybe an external library is filling in part of your class for you), you can use the definite assignment assertion operator, !
:
tsTry
classOKGreeter {// Not initialized, but no errorname !: string;}
readonly
字段可以以 readonly
修饰符作为前缀。这可以防止对构造函数之外的字段进行赋值。
¥Fields may be prefixed with the readonly
modifier.
This prevents assignments to the field outside of the constructor.
tsTry
classGreeter {readonlyname : string = "world";constructor(otherName ?: string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.= "not ok"; name }}constg = newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .= "also not ok"; name
构造器
¥Constructors
背景阅读:
构造函数(MDN)
类构造函数与函数非常相似。你可以添加带有类型注释、默认值和重载的参数:
¥Class constructors are very similar to functions. You can add parameters with type annotations, default values, and overloads:
tsTry
classPoint {x : number;y : number;// Normal signature with defaultsconstructor(x = 0,y = 0) {this.x =x ;this.y =y ;}}
tsTry
classPoint {x : number = 0;y : number = 0;// Constructor overloadsconstructor(x : number,y : number);constructor(xy : string);constructor(x : string | number,y : number = 0) {// Code logic here}}
类构造函数签名和函数签名之间只有一些区别:
¥There are just a few differences between class constructor signatures and function signatures:
-
构造函数不能有类型参数 - 这些属于外部类声明,我们稍后会了解
¥Constructors can’t have type parameters - these belong on the outer class declaration, which we’ll learn about later
-
构造函数不能有返回类型注释 - 类实例类型始终是返回的内容
¥Constructors can’t have return type annotations - the class instance type is always what’s returned
超类调用
¥Super Calls
就像在 JavaScript 中一样,如果你有一个基类,在使用任何 this.
成员之前,你需要在构造函数主体中调用 super();
:
¥Just as in JavaScript, if you have a base class, you’ll need to call super();
in your constructor body before using any this.
members:
tsTry
classBase {k = 4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.console .log (this .k );super();}}
在 JavaScript 中忘记调用 super
是一个容易犯的错误,但 TypeScript 会在必要时告诉你。
¥Forgetting to call super
is an easy mistake to make in JavaScript, but TypeScript will tell you when it’s necessary.
方法
¥Methods
背景阅读:
方法定义
类上的函数属性称为方法。方法可以使用所有与函数和构造函数相同的类型注释:
¥A function property on a class is called a method. Methods can use all the same type annotations as functions and constructors:
tsTry
classPoint {x = 10;y = 10;scale (n : number): void {this.x *=n ;this.y *=n ;}}
除了标准的类型注解,TypeScript 没有为方法添加任何新的东西。
¥Other than the standard type annotations, TypeScript doesn’t add anything else new to methods.
请注意,在方法体内,仍然必须通过 this.
访问字段和其他方法。方法主体中的非限定名称将始终引用封闭作用域内的某些内容:
¥Note that inside a method body, it is still mandatory to access fields and other methods via this.
.
An unqualified name in a method body will always refer to something in the enclosing scope:
tsTry
letx : number = 0;classC {x : string = "hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "world"; x }}
获取器/设置器
¥Getters / Setters
类也可以有访问器:
¥Classes can also have accessors:
tsTry
classC {_length = 0;getlength () {return this._length ;}setlength (value ) {this._length =value ;}}
请注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果你不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。
¥Note that a field-backed get/set pair with no extra logic is very rarely useful in JavaScript. It’s fine to expose public fields if you don’t need to add additional logic during the get/set operations.
TypeScript 对访问器有一些特殊的推断规则:
¥TypeScript has some special inference rules for accessors:
-
如果
get
存在但没有set
,则属性自动为readonly
¥If
get
exists but noset
, the property is automaticallyreadonly
-
如果不指定 setter 参数的类型,则从 getter 的返回类型推断
¥If the type of the setter parameter is not specified, it is inferred from the return type of the getter
从 TypeScript 4.3 开始,可以使用不同类型的访问器来获取和设置。
¥Since TypeScript 4.3, it is possible to have accessors with different types for getting and setting.
tsTry
classThing {_size = 0;getsize (): number {return this._size ;}setsize (value : string | number | boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size = 0;return;}this._size =num ;}}
索引签名
¥Index Signatures
类可以声明索引签名;这些工作与 其他对象类型的索引签名 相同:
¥Classes can declare index signatures; these work the same as Index Signatures for other object types:
tsTry
classMyClass {[s : string]: boolean | ((s : string) => boolean);check (s : string) {return this[s ] as boolean;}}
因为索引签名类型还需要捕获方法的类型,所以要有效地使用这些类型并不容易。通常最好将索引数据存储在另一个地方而不是类实例本身。
¥Because the index signature type needs to also capture the types of methods, it’s not easy to usefully use these types. Generally it’s better to store indexed data in another place instead of on the class instance itself.
类继承
¥Class Heritage
与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。
¥Like other languages with object-oriented features, classes in JavaScript can inherit from base classes.
implements
从句
¥implements
Clauses
你可以使用 implements
子句来检查一个类是否满足特定的 interface
。如果一个类未能正确实现它,则会触发错误:
¥You can use an implements
clause to check that a class satisfies a particular interface
.
An error will be issued if a class fails to correctly implement it:
tsTry
interfacePingable {ping (): void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.implements Ball Pingable {pong () {console .log ("pong!");}}
类也可以实现多个接口,例如 class C implements A, B {
。
¥Classes may also implement multiple interfaces, e.g. class C implements A, B {
.
注意事项
¥Cautions
重要的是要理解 implements
子句只是检查类可以被视为接口类型。它根本不会改变类的类型或其方法。常见的错误来源是假设 implements
子句将更改类类型 - 事实并非如此!
¥It’s important to understand that an implements
clause is only a check that the class can be treated as the interface type.
It doesn’t change the type of the class or its methods at all.
A common source of error is to assume that an implements
clause will change the class type - it doesn’t!
tsTry
interfaceCheckable {check (name : string): boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Notice no error herereturns .toLowerCase () === "ok";}}
在这个例子中,我们可能预计 s
的类型会受到 check
的 name: string
参数的影响。它不是 - implements
子句不会更改类主体的检查方式或其类型推断方式。
¥In this example, we perhaps expected that s
’s type would be influenced by the name: string
parameter of check
.
It is not - implements
clauses don’t change how the class body is checked or its type inferred.
同样,使用可选属性实现接口不会创建该属性:
¥Similarly, implementing an interface with an optional property doesn’t create that property:
tsTry
interfaceA {x : number;y ?: number;}classC implementsA {x = 0;}constc = newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .= 10; y
extends
从句
¥extends
Clauses
背景阅读:
扩展关键字 (MDN)
类可能来自基类。派生类具有其基类的所有属性和方法,还可以定义额外的成员。
¥Classes may extend
from a base class.
A derived class has all the properties and methods of its base class, and can also define additional members.
tsTry
classAnimal {move () {console .log ("Moving along!");}}classDog extendsAnimal {woof (times : number) {for (leti = 0;i <times ;i ++) {console .log ("woof!");}}}constd = newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
覆盖方法
¥Overriding Methods
背景阅读:
超级关键字(MDN)
派生类也可以覆盖基类字段或属性。你可以使用 super.
语法来访问基类方法。请注意,因为 JavaScript 类是一个简单的查找对象,所以没有 “超级字段” 的概念。
¥A derived class can also override a base class field or property.
You can use the super.
syntax to access base class methods.
Note that because JavaScript classes are a simple lookup object, there is no notion of a “super field”.
TypeScript 强制派生类始终是其基类的子类型。
¥TypeScript enforces that a derived class is always a subtype of its base class.
例如,这是覆盖方法的合法方式:
¥For example, here’s a legal way to override a method:
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?: string) {if (name ===undefined ) {super.greet ();} else {console .log (`Hello, ${name .toUpperCase ()}`);}}}constd = newDerived ();d .greet ();d .greet ("reader");
派生类遵循其基类契约很重要。请记住,通过基类引用来引用派生类实例是很常见的(而且总是合法的!):
¥It’s important that a derived class follow its base class contract. Remember that it’s very common (and always legal!) to refer to a derived class instance through a base class reference:
tsTry
// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
如果 Derived
不遵守 Base
的合同怎么办?
¥What if Derived
didn’t follow Base
’s contract?
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.( greet name : string) {console .log (`Hello, ${name .toUpperCase ()}`);}}
如果我们在出现错误的情况下编译此代码,则此示例将崩溃:
¥If we compiled this code despite the error, this sample would then crash:
tsTry
constb :Base = newDerived ();// Crashes because "name" will be undefinedb .greet ();
仅类型字段声明
¥Type-only Field Declarations
当 target >= ES2022
或 useDefineForClassFields
为 true
时,在父类构造函数完成后初始化类字段,覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能会成为问题。为了处理这些情况,你可以写 declare
来向 TypeScript 表明这个字段声明不应该有运行时影响。
¥When target >= ES2022
or useDefineForClassFields
is true
, class fields are initialized after the parent class constructor completes, overwriting any value set by the parent class. This can be a problem when you only want to re-declare a more accurate type for an inherited field. To handle these cases, you can write declare
to indicate to TypeScript that there should be no runtime effect for this field declaration.
tsTry
interfaceAnimal {dateOfBirth : any;}interfaceDog extendsAnimal {breed : any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Does not emit JavaScript code,// only ensures the types are correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
初始化顺序
¥Initialization Order
在某些情况下,JavaScript 类的初始化顺序可能会令人惊讶。让我们考虑这段代码:
¥The order that JavaScript classes initialize can be surprising in some cases. Let’s consider this code:
tsTry
classBase {name = "base";constructor() {console .log ("My name is " + this.name );}}classDerived extendsBase {name = "derived";}// Prints "base", not "derived"constd = newDerived ();
这里发生了什么?
¥What happened here?
JavaScript 定义的类初始化顺序是:
¥The order of class initialization, as defined by JavaScript, is:
-
基类字段被初始化
¥The base class fields are initialized
-
基类构造函数运行
¥The base class constructor runs
-
派生类字段被初始化
¥The derived class fields are initialized
-
派生类构造函数运行
¥The derived class constructor runs
这意味着基类构造函数在其自己的构造函数中看到了自己的 name
值,因为派生类字段初始化尚未运行。
¥This means that the base class constructor saw its own value for name
during its own constructor, because the derived class field initializations hadn’t run yet.
继承内置类型
¥Inheriting Built-in Types
注意:如果你不打算继承
Array
、Error
、Map
等内置类型,或者你的编译目标明确设置为ES6
/ES2015
或以上,则可以跳过本节¥Note: If you don’t plan to inherit from built-in types like
Array
,Error
,Map
, etc. or your compilation target is explicitly set toES6
/ES2015
or above, you may skip this section
在 ES2015 中,返回对象的构造函数隐式地将 this
的值替换为 super(...)
的任何调用者。生成的构造函数代码必须捕获 super(...)
的任何潜在返回值并将其替换为 this
。
¥In ES2015, constructors which return an object implicitly substitute the value of this
for any callers of super(...)
.
It is necessary for generated constructor code to capture any potential return value of super(...)
and replace it with this
.
因此,Error
、Array
和其他子类可能不再按预期工作。这是因为 Error
、Array
等的构造函数使用 ECMAScript 6 的 new.target
来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target
的值。默认情况下,其他下级编译器通常具有相同的限制。
¥As a result, subclassing Error
, Array
, and others may no longer work as expected.
This is due to the fact that constructor functions for Error
, Array
, and the like use ECMAScript 6’s new.target
to adjust the prototype chain;
however, there is no way to ensure a value for new.target
when invoking a constructor in ECMAScript 5.
Other downlevel compilers generally have the same limitation by default.
对于如下子类:
¥For a subclass like the following:
tsTry
classMsgError extendsError {constructor(m : string) {super(m );}sayHello () {return "hello " + this.message ;}}
你可能会发现:
¥you may find that:
-
方法可能是构造这些子类返回的对象上的
undefined
,所以调用sayHello
会导致错误。¥methods may be
undefined
on objects returned by constructing these subclasses, so callingsayHello
will result in an error. -
instanceof
将在子类的实例及其实例之间断开,因此(new MsgError()) instanceof MsgError
将返回false
。¥
instanceof
will be broken between instances of the subclass and their instances, so(new MsgError()) instanceof MsgError
will returnfalse
.
作为建议,你可以在任何 super(...)
调用后立即手动调整原型。
¥As a recommendation, you can manually adjust the prototype immediately after any super(...)
calls.
tsTry
classMsgError extendsError {constructor(m : string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return "hello " + this.message ;}}
但是,MsgError
的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf
的运行时,你可以改为使用 __proto__
。
¥However, any subclass of MsgError
will have to manually set the prototype as well.
For runtimes that don’t support Object.setPrototypeOf
, you may instead be able to use __proto__
.
不幸的是,这些解决方法不适用于 Internet Explorer 10 及更早版本。可以手动将原型中的方法复制到实例本身(即 MsgError.prototype
到 this
),但原型链本身无法修复。
¥Unfortunately, these workarounds will not work on Internet Explorer 10 and prior.
One can manually copy methods from the prototype onto the instance itself (i.e. MsgError.prototype
onto this
), but the prototype chain itself cannot be fixed.
成员可见性
¥Member Visibility
你可以使用 TypeScript 来控制某些方法或属性是否对类外部的代码可见。
¥You can use TypeScript to control whether certain methods or properties are visible to code outside the class.
public
类成员的默认可见性为 public
。public
成员可以在任何地方访问:
¥The default visibility of class members is public
.
A public
member can be accessed anywhere:
tsTry
classGreeter {publicgreet () {console .log ("hi!");}}constg = newGreeter ();g .greet ();
因为 public
已经是默认的可见性修饰符,所以你不需要在类成员上编写它,但出于样式/可读性的原因可能会选择这样做。
¥Because public
is already the default visibility modifier, you don’t ever need to write it on a class member, but might choose to do so for style/readability reasons.
protected
protected
成员仅对声明它们的类的子类可见。
¥protected
members are only visible to subclasses of the class they’re declared in.
tsTry
classGreeter {publicgreet () {console .log ("Hello, " + this.getName ());}protectedgetName () {return "hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// OK to access protected member hereconsole .log ("Howdy, " + this.getName ());}}constg = newSpecialGreeter ();g .greet (); // OKProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.g .(); getName
导出 protected
成员
¥Exposure of protected
members
派生类需要遵循其基类契约,但可以选择公开具有更多功能的基类子类型。这包括让 protected
成员成为 public
:
¥Derived classes need to follow their base class contracts, but may choose to expose a subtype of base class with more capabilities.
This includes making protected
members public
:
tsTry
classBase {protectedm = 10;}classDerived extendsBase {// No modifier, so default is 'public'm = 15;}constd = newDerived ();console .log (d .m ); // OK
请注意,Derived
已经能够自由读写 m
,因此这并没有有意义地改变这种情况下的 “security”。这里需要注意的主要一点是,在派生类中,如果这种暴露不是故意的,我们需要小心重复 protected
修饰符。
¥Note that Derived
was already able to freely read and write m
, so this doesn’t meaningfully alter the “security” of this situation.
The main thing to note here is that in the derived class, we need to be careful to repeat the protected
modifier if this exposure isn’t intentional.
跨层级 protected
访问
¥Cross-hierarchy protected
access
不同的 OOP 语言对于通过基类引用访问 protected
成员是否合法存在分歧:
¥Different OOP languages disagree about whether it’s legal to access a protected
member through a base class reference:
tsTry
classBase {protectedx : number = 1;}classDerived1 extendsBase {protectedx : number = 5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x = 10;}f2 (other :Derived1 ) {Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.other .= 10; x }}
例如,Java 认为这是合法的。另一方面,C# 和 C++ 选择此代码应该是非法的。
¥Java, for example, considers this to be legal. On the other hand, C# and C++ chose that this code should be illegal.
TypeScript 在这里支持 C# 和 C++,因为在 Derived2
中访问 x
应该只在 Derived2
的子类中是合法的,而 Derived1
不是其中之一。此外,如果通过 Derived1
引用访问 x
是非法的(当然应该如此!),那么通过基类引用访问它永远不会改善这种情况。
¥TypeScript sides with C# and C++ here, because accessing x
in Derived2
should only be legal from Derived2
’s subclasses, and Derived1
isn’t one of them.
Moreover, if accessing x
through a Derived1
reference is illegal (which it certainly should be!), then accessing it through a base class reference should never improve the situation.
另请参阅 为什么我不能从派生类访问受保护的成员?,它解释了 C# 的更多推断。
¥See also Why Can’t I Access A Protected Member From A Derived Class? which explains more of C#‘s reasoning.
private
private
类似于 protected
,但不允许从子类访问成员:
¥private
is like protected
, but doesn’t allow access to the member even from subclasses:
tsTry
classBase {privatex = 0;}constb = newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTry
classDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
因为 private
成员对派生类不可见,所以派生类不能增加它们的可见性:
¥Because private
members aren’t visible to derived classes, a derived class can’t increase their visibility:
tsTry
classBase {privatex = 0;}classClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.extends Derived Base {x = 1;}
跨实例 private
访问
¥Cross-instance private
access
不同的 OOP 语言对于同一类的不同实例是否可以访问彼此的 private
成员存在分歧。虽然 Java、C#、C++、Swift 和 PHP 等语言允许这样做,但 Ruby 不允许。
¥Different OOP languages disagree about whether different instances of the same class may access each others’ private
members.
While languages like Java, C#, C++, Swift, and PHP allow this, Ruby does not.
TypeScript 确实允许跨实例 private
访问:
¥TypeScript does allow cross-instance private
access:
tsTry
classA {privatex = 10;publicsameAs (other :A ) {// No errorreturnother .x === this.x ;}}
警告
¥Caveats
与 TypeScript 类型系统的其他方面一样,private
和 protected
仅在类型检查期间强制执行。
¥Like other aspects of TypeScript’s type system, private
and protected
are only enforced during type checking.
这意味着 in
或简单属性查找之类的 JavaScript 运行时构造仍然可以访问 private
或 protected
成员:
¥This means that JavaScript runtime constructs like in
or simple property lookup can still access a private
or protected
member:
tsTry
classMySafe {privatesecretKey = 12345;}
js
// In a JavaScript file...const s = new MySafe();// Will print 12345console.log(s.secretKey);
private
还允许在类型检查期间使用括号表示法进行访问。这使得 private
声明的字段可能更容易访问,例如单元测试,缺点是这些字段是软私有的并且不严格执行隐私。
¥private
also allows access using bracket notation during type checking. This makes private
-declared fields potentially easier to access for things like unit tests, with the drawback that these fields are soft private and don’t strictly enforce privacy.
tsTry
classMySafe {privatesecretKey = 12345;}consts = newMySafe ();// Not allowed during type checkingProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
与 TypeScripts 的 private
不同,JavaScript 的 私有字段 (#
) 在编译后仍然是私有的,并且不提供前面提到的像括号符号访问这样的转义舱口,这使得它们很难私有。
¥Unlike TypeScripts’s private
, JavaScript’s private fields (#
) remain private after compilation and do not provide the previously mentioned escape hatches like bracket notation access, making them hard private.
tsTry
classDog {#barkAmount = 0;personality = "happy";constructor() {}}
tsTry
"use strict";class Dog {#barkAmount = 0;personality = "happy";constructor() { }}
当编译到 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 #
。
¥When compiling to ES2021 or less, TypeScript will use WeakMaps in place of #
.
tsTry
"use strict";var _Dog_barkAmount;class Dog {constructor() {_Dog_barkAmount.set(this, 0);this.personality = "happy";}}_Dog_barkAmount = new WeakMap();
如果你需要保护类中的值免受恶意行为者的侵害,你应该使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。
¥If you need to protect values in your class from malicious actors, you should use mechanisms that offer hard runtime privacy, such as closures, WeakMaps, or private fields. Note that these added privacy checks during runtime could affect performance.
静态成员
¥Static Members
背景阅读:
静态成员 (MDN)
类可能有 static
个成员。这些成员不与类的特定实例相关联。它们可以通过类构造函数对象本身访问:
¥Classes may have static
members.
These members aren’t associated with a particular instance of the class.
They can be accessed through the class constructor object itself:
tsTry
classMyClass {staticx = 0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
静态成员也可以使用相同的 public
、protected
和 private
可见性修饰符:
¥Static members can also use the same public
, protected
, and private
visibility modifiers:
tsTry
classMyClass {private staticx = 0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
静态成员也被继承:
¥Static members are also inherited:
tsTry
classBase {staticgetGreeting () {return "Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
特殊静态名称
¥Special Static Names
从 Function
原型覆盖属性通常是不安全/不可能的。因为类本身就是可以用 new
调用的函数,所以不能使用某些 static
名称。name
、length
和 call
等函数属性无法定义为 static
成员:
¥It’s generally not safe/possible to overwrite properties from the Function
prototype.
Because classes are themselves functions that can be invoked with new
, certain static
names can’t be used.
Function properties like name
, length
, and call
aren’t valid to define as static
members:
tsTry
classS {staticStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.= "S!"; name }
为什么没有静态类?
¥Why No Static Classes?
TypeScript(和 JavaScript)没有一个名为 static class
的构造,就像 C# 一样。
¥TypeScript (and JavaScript) don’t have a construct called static class
the same way as, for example, C# does.
这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为 TypeScript 中不存在该限制,所以不需要它们。只有一个实例的类通常只表示为 JavaScript/TypeScript 中的普通对象。
¥Those constructs only exist because those languages force all data and functions to be inside a class; because that restriction doesn’t exist in TypeScript, there’s no need for them. A class with only a single instance is typically just represented as a normal object in JavaScript/TypeScript.
例如,我们不需要 TypeScript 中的 “静态类” 语法,因为常规对象(甚至顶层函数)也可以完成这项工作:
¥For example, we don’t need a “static class” syntax in TypeScript because a regular object (or even top-level function) will do the job just as well:
tsTry
// Unnecessary "static" classclassMyStaticClass {staticdoSomething () {}}// Preferred (alternative 1)functiondoSomething () {}// Preferred (alternative 2)constMyHelperObject = {dosomething () {},};
static
类中的块
¥static
Blocks in Classes
静态块允许你编写具有自己作用域的语句序列,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有编写语句的所有功能的初始化代码,不会泄漏变量,并且可以完全访问我们类的内部结构。
¥Static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class. This means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class’s internals.
tsTry
classFoo {static #count = 0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
泛型类
¥Generic Classes
类,很像接口,可以是泛型的。当使用 new
实例化泛型类时,其类型参数的推断方式与函数调用中的方式相同:
¥Classes, much like interfaces, can be generic.
When a generic class is instantiated with new
, its type parameters are inferred the same way as in a function call:
tsTry
classBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb = newBox ("hello!");
类可以像接口一样使用泛型约束和默认值。
¥Classes can use generic constraints and defaults the same way as interfaces.
静态成员中的类型参数
¥Type Parameters in Static Members
此代码不合法,原因可能并不明显:
¥This code isn’t legal, and it may not be obvious why:
tsTry
classBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
请记住,类型总是被完全擦除!在运行时,只有一个 Box.defaultValue
属性槽。这意味着设置 Box<string>.defaultValue
(如果可能的话)也会更改 Box<number>.defaultValue
- 不好。泛型类的 static
成员永远不能引用类的类型参数。
¥Remember that types are always fully erased!
At runtime, there’s only one Box.defaultValue
property slot.
This means that setting Box<string>.defaultValue
(if that were possible) would also change Box<number>.defaultValue
- not good.
The static
members of a generic class can never refer to the class’s type parameters.
类运行时的 this
¥this
at Runtime in Classes
背景阅读:
这个关键字(MDN)
重要的是要记住,TypeScript 不会改变 JavaScript 的运行时行为,并且 JavaScript 以具有一些特殊的运行时行为而闻名。
¥It’s important to remember that TypeScript doesn’t change the runtime behavior of JavaScript, and that JavaScript is somewhat famous for having some peculiar runtime behaviors.
JavaScript 对 this
的处理确实不寻常:
¥JavaScript’s handling of this
is indeed unusual:
tsTry
classMyClass {name = "MyClass";getName () {return this.name ;}}constc = newMyClass ();constobj = {name : "obj",getName :c .getName ,};// Prints "obj", not "MyClass"console .log (obj .getName ());
长话短说,默认情况下,函数中 this
的值取决于函数的调用方式。在这个例子中,因为函数是通过 obj
引用调用的,所以它的 this
的值是 obj
而不是类实例。
¥Long story short, by default, the value of this
inside a function depends on how the function was called.
In this example, because the function was called through the obj
reference, its value of this
was obj
rather than the class instance.
这很少是你想要发生的!TypeScript 提供了一些减轻或防止此类错误的方法。
¥This is rarely what you want to happen! TypeScript provides some ways to mitigate or prevent this kind of error.
箭头函数
¥Arrow Functions
背景阅读:
箭头函数 (MDN)
如果你有一个经常以丢失其 this
上下文的方式调用的函数,则使用箭头函数属性而不是方法定义是有意义的:
¥If you have a function that will often be called in a way that loses its this
context, it can make sense to use an arrow function property instead of a method definition:
tsTry
classMyClass {name = "MyClass";getName = () => {return this.name ;};}constc = newMyClass ();constg =c .getName ;// Prints "MyClass" instead of crashingconsole .log (g ());
这有一些权衡:
¥This has some trade-offs:
-
this
值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此¥The
this
value is guaranteed to be correct at runtime, even for code not checked with TypeScript -
这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本
¥This will use more memory, because each class instance will have its own copy of each function defined this way
-
你不能在派生类中使用
super.getName
,因为原型链中没有条目可以从中获取基类方法¥You can’t use
super.getName
in a derived class, because there’s no entry in the prototype chain to fetch the base class method from
this
参数
¥this
parameters
在方法或函数定义中,名为 this
的初始参数在 TypeScript 中具有特殊含义。这些参数在编译期间被删除:
¥In a method or function definition, an initial parameter named this
has special meaning in TypeScript.
These parameters are erased during compilation:
tsTry
// TypeScript input with 'this' parameterfunctionfn (this :SomeType ,x : number) {/* ... */}
js
// JavaScript outputfunction fn(x) {/* ... */}
TypeScript 检查是否使用正确的上下文调用带有 this
参数的函数。我们可以不使用箭头函数,而是在方法定义中添加一个 this
参数,以静态强制方法被正确调用:
¥TypeScript checks that calling a function with a this
parameter is done so with a correct context.
Instead of using an arrow function, we can add a this
parameter to method definitions to statically enforce that the method is called correctly:
tsTry
classMyClass {name = "MyClass";getName (this :MyClass ) {return this.name ;}}constc = newMyClass ();// OKc .getName ();// Error, would crashconstg =c .getName ;The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.console .log (g ());
此方法与箭头函数方法进行了相反的权衡:
¥This method makes the opposite trade-offs of the arrow function approach:
-
JavaScript 调用者可能仍然不正确地使用类方法而没有意识到
¥JavaScript callers might still use the class method incorrectly without realizing it
-
每个类定义只分配一个函数,而不是每个类实例一个
¥Only one function per class definition gets allocated, rather than one per class instance
-
仍然可以通过
super
调用基本方法定义。¥Base method definitions can still be called via
super
.
this
类型
¥this
Types
在类中,一种称为 this
的特殊类型动态地引用当前类的类型。让我们看看这有什么用处:
¥In classes, a special type called this
refers dynamically to the type of the current class.
Let’s see how this is useful:
tsTry
classBox {contents : string = "";set (value : string) {this.contents =value ;return this;}}
在这里,TypeScript 推断 set
的返回类型为 this
,而不是 Box
。现在让我们创建一个 Box
的子类:
¥Here, TypeScript inferred the return type of set
to be this
, rather than Box
.
Now let’s make a subclass of Box
:
tsTry
classClearableBox extendsBox {clear () {this.contents = "";}}consta = newClearableBox ();constb =a .set ("hello");
你还可以在参数类型注释中使用 this
:
¥You can also use this
in a parameter type annotation:
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}
这与编写 other: Box
不同 - 如果你有一个派生类,它的 sameAs
方法现在将只接受同一个派生类的其他实例:
¥This is different from writing other: Box
— if you have a derived class, its sameAs
method will now only accept other instances of that same derived class:
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}classDerivedBox extendsBox {otherContent : string = "?";}constbase = newBox ();constderived = newDerivedBox ();Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.derived .sameAs (); base
this
型防护
¥this
-based type guards
你可以在类和接口中的方法的返回位置使用 this is Type
。当与类型缩小(例如 if
语句)混合时,目标对象的类型将缩小到指定的 Type
。
¥You can use this is Type
in the return position for methods in classes and interfaces.
When mixed with a type narrowing (e.g. if
statements) the type of the target object would be narrowed to the specified Type
.
tsTry
classFileSystemObject {isFile (): this isFileRep {return this instanceofFileRep ;}isDirectory (): this isDirectory {return this instanceofDirectory ;}isNetworked (): this isNetworked & this {return this.networked ;}constructor(publicpath : string, privatenetworked : boolean) {}}classFileRep extendsFileSystemObject {constructor(path : string, publiccontent : string) {super(path , false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host : string;}constfso :FileSystemObject = newFileRep ("foo/bar.txt", "foo");if (fso .isFile ()) {fso .content ;} else if (fso .isDirectory ()) {fso .children ;} else if (fso .isNetworked ()) {fso .host ;}
基于 this 的类型保护的一个常见用例是允许对特定字段进行延迟验证。例如,当 hasValue
被验证为真时,这种情况会从框中保存的值中删除 undefined
:
¥A common use-case for a this-based type guard is to allow for lazy validation of a particular field. For example, this case removes an undefined
from the value held inside box when hasValue
has been verified to be true:
tsTry
classBox <T > {value ?:T ;hasValue (): this is {value :T } {return this.value !==undefined ;}}constbox = newBox <string>();box .value = "Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
参数属性
¥Parameter Properties
TypeScript 提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些称为参数属性,是通过在构造函数参数前加上可见性修饰符 public
、private
、protected
或 readonly
之一来创建的。结果字段获取这些修饰符:
¥TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value.
These are called parameter properties and are created by prefixing a constructor argument with one of the visibility modifiers public
, private
, protected
, or readonly
.
The resulting field gets those modifier(s):
tsTry
classParams {constructor(public readonlyx : number,protectedy : number,privatez : number) {// No body necessary}}consta = newParams (1, 2, 3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
类表达式
¥Class Expressions
背景阅读:
类表达式 (MDN)
类表达式与类声明非常相似。唯一真正的区别是类表达式不需要名称,尽管我们可以通过它们最终绑定到的任何标识符来引用它们:
¥Class expressions are very similar to class declarations. The only real difference is that class expressions don’t need a name, though we can refer to them via whatever identifier they ended up bound to:
tsTry
constsomeClass = class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm = newsomeClass ("Hello, world");
构造函数签名
¥Constructor Signatures
JavaScript 类是使用 new
运算符实例化的。给定类本身的类型,InstanceType 工具类型会对此操作进行建模。
¥JavaScript classes are instantiated with the new
operator. Given the type of a class itself, the InstanceType utility type models this operation.
tsTry
classPoint {createdAt : number;x : number;y : numberconstructor(x : number,y : number) {this.createdAt =Date .now ()this.x =x ;this.y =y ;}}typePointInstance =InstanceType <typeofPoint >functionmoveRight (point :PointInstance ) {point .x += 5;}constpoint = newPoint (3, 4);moveRight (point );point .x ; // => 8
abstract
类和成员
¥abstract
Classes and Members
TypeScript 中的类、方法和字段可能是抽象的。
¥Classes, methods, and fields in TypeScript may be abstract.
抽象方法或抽象字段是尚未提供实现的方法。这些成员必须存在于抽象类中,不能直接实例化。
¥An abstract method or abstract field is one that hasn’t had an implementation provided. These members must exist inside an abstract class, which cannot be directly instantiated.
抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,就说它是具体的。
¥The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract members. When a class doesn’t have any abstract members, it is said to be concrete.
让我们看一个例子:
¥Let’s look at an example:
tsTry
abstract classBase {abstractgetName (): string;printName () {console .log ("Hello, " + this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b = newBase ();
我们不能用 new
实例化 Base
,因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员:
¥We can’t instantiate Base
with new
because it’s abstract.
Instead, we need to make a derived class and implement the abstract members:
tsTry
classDerived extendsBase {getName () {return "world";}}constd = newDerived ();d .printName ();
请注意,如果我们忘记实现基类的抽象成员,我们会得到一个错误:
¥Notice that if we forget to implement the base class’s abstract members, we’ll get an error:
tsTry
classNon-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.extends Derived Base {// forgot to do anything}
抽象构造签名
¥Abstract Construct Signatures
有时你想接受一些类构造函数,它产生一个派生自某个抽象类的类的实例。
¥Sometimes you want to accept some class constructor function that produces an instance of a class which derives from some abstract class.
例如,你可能想编写以下代码:
¥For example, you might want to write this code:
tsTry
functiongreet (ctor : typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance = newctor ();instance .printName ();}
TypeScript 正确地告诉你你正在尝试实例化一个抽象类。毕竟,给定 greet
的定义,编写这段代码是完全合法的,它最终会构造一个抽象类:
¥TypeScript is correctly telling you that you’re trying to instantiate an abstract class.
After all, given the definition of greet
, it’s perfectly legal to write this code, which would end up constructing an abstract class:
tsTry
// Bad!greet (Base );
相反,你想编写一个接受带有构造签名的东西的函数:
¥Instead, you want to write a function that accepts something with a construct signature:
tsTry
functiongreet (ctor : new () =>Base ) {constinstance = newctor ();instance .printName ();}greet (Derived );Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.greet (); Base
现在 TypeScript 可以正确告诉你可以调用哪些类构造函数 - Derived
可以,因为它是具体的,但 Base
不能。
¥Now TypeScript correctly tells you about which class constructor functions can be invoked - Derived
can because it’s concrete, but Base
cannot.
类之间的关系
¥Relationships Between Classes
在大多数情况下,TypeScript 中的类在结构上进行比较,与其他类型相同。
¥In most cases, classes in TypeScript are compared structurally, the same as other types.
例如,这两个类可以互相代替使用,因为它们是相同的:
¥For example, these two classes can be used in place of each other because they’re identical:
tsTry
classPoint1 {x = 0;y = 0;}classPoint2 {x = 0;y = 0;}// OKconstp :Point1 = newPoint2 ();
同样,即使没有显式继承,类之间的子类型关系也存在:
¥Similarly, subtype relationships between classes exist even if there’s no explicit inheritance:
tsTry
classPerson {name : string;age : number;}classEmployee {name : string;age : number;salary : number;}// OKconstp :Person = newEmployee ();
这听起来很简单,但有一些案例似乎比其他案例更奇怪。
¥This sounds straightforward, but there are a few cases that seem stranger than others.
空类没有成员。在结构类型系统中,没有成员的类型通常是其他任何东西的超类型。所以如果你写一个空类(不要!),任何东西都可以用来代替它:
¥Empty classes have no members. In a structural type system, a type with no members is generally a supertype of anything else. So if you write an empty class (don’t!), anything can be used in place of it:
tsTry
classEmpty {}functionfn (x :Empty ) {// can't do anything with 'x', so I won't}// All OK!fn (window );fn ({});fn (fn );