static#(私有)get(获取器)和 set(设置器)*(生成器)asyncpublic、private 或 protected在本章中,我们将研究类定义在 TypeScript 中的工作原理。
本节是纯 JavaScript 中类定义的备忘单。
class OtherClass {}
class MyClass1 extends OtherClass {
publicInstanceField = 1;
constructor() {
super();
}
publicPrototypeMethod() {
return 2;
}
}
const inst1 = new MyClass1();
assert.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2); 以下部分介绍修饰符
最后,有一个表格显示了如何组合修饰符。
staticclass MyClass2 {
static staticPublicField = 1;
static staticPublicMethod() {
return 2;
}
}
assert.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2);#(私有)class MyClass3 {
#privateField = 1;
#privateMethod() {
return 2;
}
static accessPrivateMembers() {
// Private members can only be accessed from inside class definitions
const inst3 = new MyClass3();
assert.equal(inst3.#privateField, 1);
assert.equal(inst3.#privateMethod(), 2);
}
}
MyClass3.accessPrivateMembers();JavaScript 警告
TypeScript 从 3.8 版本开始支持私有字段,但目前不支持私有方法。
get(获取器)和 set(设置器)粗略地说,访问器是通过访问属性调用的方法。访问器有两种:获取器和设置器。
class MyClass5 {
#name = 'Rumpelstiltskin';
/** Prototype getter */
get name() {
return this.#name;
}
/** Prototype setter */
set name(value) {
this.#name = value;
}
}
const inst5 = new MyClass5();
assert.equal(inst5.name, 'Rumpelstiltskin'); // getter
inst5.name = 'Queen'; // setter
assert.equal(inst5.name, 'Queen'); // getter*(生成器)class MyClass6 {
* publicPrototypeGeneratorMethod() {
yield 'hello';
yield 'world';
}
}
const inst6 = new MyClass6();
assert.deepEqual(
[...inst6.publicPrototypeGeneratorMethod()],
['hello', 'world']);asyncclass MyClass7 {
async publicPrototypeAsyncMethod() {
const result = await Promise.resolve('abc');
return result + result;
}
}
const inst7 = new MyClass7();
inst7.publicPrototypeAsyncMethod()
.then(result => assert.equal(result, 'abcabc'));const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');
class MyClass8 {
[publicInstanceFieldKey] = 1;
[publicPrototypeMethodKey]() {
return 2;
}
}
const inst8 = new MyClass8();
assert.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2);注释
Symbol.iterator。但是,方括号内可以使用任何表达式。字段(无级别表示构造存在于实例级别)
| 级别 | 可见性 |
|---|---|
| (实例) | |
| (实例) | # |
静态 |
|
静态 |
# |
方法(无级别表示构造存在于原型级别)
| 级别 | 访问器 | 异步 | 生成器 | 可见性 |
|---|---|---|---|---|
| (原型) | ||||
| (原型) | 获取 |
|||
| (原型) | 设置 |
|||
| (原型) | 异步 |
|||
| (原型) | * |
|||
| (原型) | 异步 |
* |
||
| (与原型关联) | # |
|||
| (与原型关联) | 获取 |
# |
||
| (与原型关联) | 设置 |
# |
||
| (与原型关联) | 异步 |
# |
||
| (与原型关联) | * |
# |
||
| (与原型关联) | 异步 |
* |
# |
|
静态 |
||||
静态 |
获取 |
|||
静态 |
设置 |
|||
静态 |
异步 |
|||
静态 |
* |
|||
静态 |
异步 |
* |
||
静态 |
# |
|||
静态 |
获取 |
# |
||
静态 |
设置 |
# |
||
静态 |
异步 |
# |
||
静态 |
* |
# |
||
静态 |
异步 |
* |
# |
方法的限制
重要的是要记住,对于类,有两条原型对象链。
请考虑以下纯 JavaScript 示例。
class ClassA {
static staticMthdA() {}
constructor(instPropA) {
this.instPropA = instPropA;
}
prototypeMthdA() {}
}
class ClassB extends ClassA {
static staticMthdB() {}
constructor(instPropA, instPropB) {
super(instPropA);
this.instPropB = instPropB;
}
prototypeMthdB() {}
}
const instB = new ClassB(0, 1);图 1 显示了由 ClassA 和 ClassB 创建的原型链的外观。
默认情况下,TypeScript 中的所有数据槽都是公共属性。有两种方法可以保持数据的私密性。
接下来我们将分别介绍这两种方法。
请注意,TypeScript 目前不支持私有方法。
私有属性是仅限 TypeScript(静态)的特性。任何属性都可以通过在其前面加上关键字 private 来设为私有(A 行)。
class PersonPrivateProperty {
private name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}如果我们在错误的作用域中访问该属性,现在会收到编译时错误(A 行)。
const john = new PersonPrivateProperty('John');
assert.equal(
john.sayHello(), 'Hello John!');
// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
john.name; // (A)但是,private 在运行时不会改变任何内容。在那里,属性 .name 与公共属性没有区别。
assert.deepEqual(
Object.keys(john),
['name']);当我们查看类编译成的 JavaScript 代码时,我们也可以看到私有属性在运行时不受保护。
class PersonPrivateProperty {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}私有字段是 TypeScript 从 3.8 版本开始支持的一项新的 JavaScript 特性。
class PersonPrivateField {
#name: string;
constructor(name: string) {
this.#name = name;
}
sayHello() {
return `Hello ${this.#name}!`;
}
}此版本的 Person 的使用方式与私有属性版本基本相同。
const john = new PersonPrivateField('John');
assert.equal(
john.sayHello(), 'Hello John!');但是,这一次,数据是完全封装的。在类外部使用私有字段语法甚至是一个 JavaScript 语法错误。这就是为什么我们必须在 A 行中使用 eval(),以便我们可以执行此代码。
assert.throws(
() => eval('john.#name'), // (A)
{
name: 'SyntaxError',
message: "Private field '#name' must be declared in "
+ "an enclosing class",
});
assert.deepEqual(
Object.keys(john),
[]);编译结果现在要复杂得多(略有简化)。
var __classPrivateFieldSet = function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError(
'attempted to set private field on non-instance');
}
privateMap.set(receiver, value);
return value;
};
// Omitted: __classPrivateFieldGet
var _name = new WeakMap();
class Person {
constructor(name) {
// Add an entry for this instance to _name
_name.set(this, void 0);
// Now we can use the helper function:
__classPrivateFieldSet(this, _name, name);
}
// ···
}此代码使用了一种常见的技术来保持实例数据的私密性。
有关此主题的更多信息,请参阅“面向急于求成的程序员的 JavaScript”。
私有字段和私有属性不能在子类中访问(A 行)。
class PrivatePerson {
private name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class PrivateEmployee extends PrivatePerson {
private company: string;
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
// @ts-expect-error: Property 'name' is private and only
// accessible within class 'PrivatePerson'. (2341)
return `Hello ${this.name} from ${this.company}!`; // (A)
}
}我们可以通过在 A 行中将 private 更改为 protected 来修复前面的示例(为了保持一致性,我们也在 B 行中进行了更改)。
class ProtectedPerson {
protected name: string; // (A)
constructor(name: string) {
this.name = name;
}
sayHello() {
return `Hello ${this.name}!`;
}
}
class ProtectedEmployee extends ProtectedPerson {
protected company: string; // (B)
constructor(name: string, company: string) {
super(name);
this.company = company;
}
sayHello() {
return `Hello ${this.name} from ${this.company}!`; // OK
}
}构造函数也可以是私有的。当我们有静态工厂方法并希望客户端始终使用这些方法,而从不直接使用构造函数时,这很有用。静态方法可以访问私有类成员,这就是为什么工厂方法仍然可以使用构造函数的原因。
在以下代码中,有一个静态工厂方法 DataContainer.create()。它通过异步加载的数据设置实例。在工厂方法中保留异步代码可以使实际类完全同步。
class DataContainer {
#data: string;
static async create() {
const data = await Promise.resolve('downloaded'); // (A)
return new this(data);
}
private constructor(data: string) {
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));在实际代码中,我们将使用 fetch() 或类似的基于 Promise 的 API 在 A 行中异步加载数据。
私有构造函数阻止 DataContainer 被子类化。如果我们想允许子类,我们必须将其设为 protected。
如果打开了编译器设置 --strictPropertyInitialization(如果我们使用 --strict,则默认打开),则 TypeScript 会检查是否正确初始化了所有声明的实例属性。
通过构造函数中的赋值。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}或者通过属性声明的初始化器。
class Point {
x = 0;
y = 0;
// No constructor needed
}但是,有时我们以 TypeScript 无法识别的方式初始化属性。然后,我们可以使用感叹号(*确定赋值断言*)来关闭 TypeScript 的警告(A 行和 B 行)。
class Point {
x!: number; // (A)
y!: number; // (B)
constructor() {
this.initProperties();
}
initProperties() {
this.x = 0;
this.y = 0;
}
}在以下示例中,我们还需要确定赋值断言。在这里,我们通过构造函数参数 props 设置实例属性。
class CompilerError implements CompilerErrorProps { // (A)
line!: number;
description!: string;
constructor(props: CompilerErrorProps) {
Object.assign(this, props); // (B)
}
}
// Helper interface for the parameter properties
interface CompilerErrorProps {
line: number,
description: string,
}
// Using the class:
const err = new CompilerError({
line: 123,
description: 'Unexpected token',
});注意
Object.assign() 将参数 props 的属性复制到 this 中。implements 确保类声明了接口 CompilerErrorProps 中的所有属性。public、private 或 protected如果我们对构造函数参数使用关键字 public,则 TypeScript 会为我们做两件事。
因此,以下两个类是等效的。
class Point1 {
constructor(public x: number, public y: number) {
}
}
class Point2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}如果我们使用 private 或 protected 而不是 public,则相应的实例属性是私有的或受保护的(而不是公共的)。
在 TypeScript 中,两种构造可以是抽象的。
以下代码演示了抽象类和方法。
一方面,有抽象超类 Printable 及其辅助类 StringBuilder。
class StringBuilder {
string = '';
add(str: string) {
this.string += str;
}
}
abstract class Printable {
toString() {
const out = new StringBuilder();
this.print(out);
return out.string;
}
abstract print(out: StringBuilder): void;
}另一方面,有具体子类 Entries 和 Entry。
class Entries extends Printable {
entries: Entry[];
constructor(entries: Entry[]) {
super();
this.entries = entries;
}
print(out: StringBuilder): void {
for (const entry of this.entries) {
entry.print(out);
}
}
}
class Entry extends Printable {
key: string;
value: string;
constructor(key: string, value: string) {
super();
this.key = key;
this.value = value;
}
print(out: StringBuilder): void {
out.add(this.key);
out.add(': ');
out.add(this.value);
out.add('\n');
}
}最后,这是我们使用 Entries 和 Entry 的方式。
const entries = new Entries([
new Entry('accept-ranges', 'bytes'),
new Entry('content-length', '6518'),
]);
assert.equal(
entries.toString(),
'accept-ranges: bytes\ncontent-length: 6518\n');关于抽象类的注释