static
#
(私有)get
(取得器)和set
(設定器)*
(產生器)async
public
、private
或protected
在本章中,我們將探討 TypeScript 中的類別定義如何運作
本節是純粹 JavaScript 中類別定義的秘笈。
class OtherClass {}
class MyClass1 extends OtherClass {
= 1;
publicInstanceField
constructor() {
super();
}
publicPrototypeMethod() {
return 2;
}
}
const inst1 = new MyClass1();
.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2); assert
下一節是關於修飾詞
最後,有一個表格顯示修飾詞如何組合。
static
class MyClass2 {
static staticPublicField = 1;
static staticPublicMethod() {
return 2;
}
}
.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2); assert
#
(私人)class MyClass3 {
= 1;
#privateField
privateMethod() {
#return 2;
}
static accessPrivateMembers() {
// Private members can only be accessed from inside class definitions
const inst3 = new MyClass3();
.equal(inst3.#privateField, 1);
assert.equal(inst3.#privateMethod(), 2);
assert
}
}.accessPrivateMembers(); MyClass3
JavaScript 警告
TypeScript 自 3.8 版起支援私人欄位,但目前不支援私人方法。
get
(取得器)和 set
(設定器)大致來說,存取器是透過存取屬性來呼叫的方法。有兩種存取器:取得器和設定器。
class MyClass5 {
= 'Rumpelstiltskin';
#name
/** Prototype getter */
name() {
get return this.#name;
}
/** Prototype setter */
name(value) {
set this.#name = value;
}
}const inst5 = new MyClass5();
.equal(inst5.name, 'Rumpelstiltskin'); // getter
assert.name = 'Queen'; // setter
inst5.equal(inst5.name, 'Queen'); // getter assert
*
(產生器)class MyClass6 {
* publicPrototypeGeneratorMethod() {
yield 'hello';
yield 'world';
}
}
const inst6 = new MyClass6();
.deepEqual(
assert...inst6.publicPrototypeGeneratorMethod()],
['hello', 'world']); [
async
class MyClass7 {
async publicPrototypeAsyncMethod() {
const result = await Promise.resolve('abc');
return result + result;
}
}
const inst7 = new MyClass7();
.publicPrototypeAsyncMethod()
inst7.then(result => assert.equal(result, 'abcabc'));
const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');
class MyClass8 {
= 1;
[publicInstanceFieldKey]
[publicPrototypeMethodKey]() {return 2;
}
}
const inst8 = new MyClass8();
.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2); assert
註解
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 {: string; // (A)
private nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
} }
如果我們在錯誤的範圍內存取該屬性,我們現在會收到編譯時期錯誤(A 行)
= new PersonPrivateProperty('John');
const john
.equal(
assert.sayHello(), 'Hello John!');
john
// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
.name; // (A) john
但是,private
沒有在執行時期改變任何東西。在執行時期,屬性 .name
和公開屬性沒有區別
.deepEqual(
assert.keys(john),
Object'name']); [
當我們查看類別編譯成的 JavaScript 程式碼時,我們也可以看到私有屬性在執行時期並未受到保護
class PersonPrivateProperty {
constructor(name) {
this.name = name;
}sayHello() {
return `Hello ${this.name}!`;
} }
私有欄位是 TypeScript 自 3.8 版以來支援的一項新的 JavaScript 功能
class PersonPrivateField {: string;
#nameconstructor(name: string) {
.#name = name;
this
}sayHello() {
return `Hello ${this.#name}!`;
} }
這個版本的 Person
大多和私有屬性版本以相同的方式使用
= new PersonPrivateField('John');
const john
.equal(
assert.sayHello(), 'Hello John!'); john
不過,這次資料是完全封裝的。在類別外部使用私有欄位語法甚至會造成 JavaScript 語法錯誤。這就是為什麼我們必須在 A 行使用 eval()
,才能執行這段程式碼
.throws(
assert=> eval('john.#name'), // (A)
()
{: 'SyntaxError',
name: "Private field '#name' must be declared in "
message+ "an enclosing class",
;
})
.deepEqual(
assert.keys(john),
Object; [])
編譯結果現在複雜得多(略作簡化)
var __classPrivateFieldSet = function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError(
'attempted to set private field on non-instance');
}.set(receiver, value);
privateMapreturn value;
;
}
// Omitted: __classPrivateFieldGet
var _name = new WeakMap();
class Person {
constructor(name) {
// Add an entry for this instance to _name
.set(this, void 0);
_name
// Now we can use the helper function:
__classPrivateFieldSet(this, _name, name);
}// ···
}
這段程式碼使用了一種常見的技術來讓執行個體資料保持私密
有關此主題的更多資訊:請參閱 “JavaScript for impatient programmers”。
無法在子類別中存取私有欄位和私有屬性(A 行)
class PrivatePerson {: string;
private nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
}
}
class PrivateEmployee extends PrivatePerson {: string;
private companyconstructor(name: string, company: string) {
super(name);
.company = company;
this
}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 {: string; // (A)
protected nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
}
}
class ProtectedEmployee extends ProtectedPerson {: string; // (B)
protected companyconstructor(name: string, company: string) {
super(name);
.company = company;
this
}sayHello() {
return `Hello ${this.name} from ${this.company}!`; // OK
} }
建構函式也可以是私有的。當我們有靜態工廠方法,而且希望用戶端總是使用這些方法,而不要直接使用建構函式時,這會很有用。靜態方法可以存取私有類別成員,這就是為什麼工廠方法仍然可以使用建構函式的緣故。
在以下程式碼中,有一個靜態工廠方法 DataContainer.create()
。它透過非同步載入的資料設定執行個體。將非同步程式碼保留在工廠方法中,可以讓實際類別完全同步
class DataContainer {: string;
#datacreate() {
static async = await Promise.resolve('downloaded'); // (A)
const data new this(data);
return
}constructor(data: string) {
private .#data = data;
this
}getData() {
'DATA: '+this.#data;
return
}
}.create()
DataContainer.then(dc => assert.equal(
.getData(), 'DATA: downloaded')); dc
在實際程式碼中,我們會在 A 行使用 fetch()
或類似的基於 Promise 的 API 來非同步載入資料。
私有建構函式可防止 DataContainer
被子類別化。如果我們要允許子類別,我們必須將其設為 protected
。
如果編譯器設定 --strictPropertyInitialization
已開啟(如果我們使用 --strict
,則為這種情況),則 TypeScript 會檢查是否所有宣告的實例屬性都已正確初始化
透過建構函式中的指定
class Point {: number;
x: number;
yconstructor(x: number, y: number) {
.x = x;
this.y = y;
this
} }
或透過屬性宣告的初始化函式
class Point {= 0;
x = 0;
y
// No constructor needed
}
然而,有時我們會以 TypeScript 無法辨識的方式初始化屬性。然後,我們可以使用驚嘆號(明確指定)來關閉 TypeScript 的警告(A 行和 B 行)
class Point {!: number; // (A)
x!: number; // (B)
yconstructor() {
.initProperties();
this
}initProperties() {
.x = 0;
this.y = 0;
this
} }
在以下範例中,我們也需要明確指定。在此,我們透過建構函式參數 props
設定實例屬性
// (A)
class CompilerError implements CompilerErrorProps { !: number;
line!: string;
descriptionconstructor(props: CompilerErrorProps) {
.assign(this, props); // (B)
Object
}
}
// Helper interface for the parameter properties
interface CompilerErrorProps {: number,
line: string,
description
}
// Using the class:
= new CompilerError({
const err : 123,
line: 'Unexpected token',
description; })
備註
Object.assign()
將參數 props
的屬性複製到 this
。implements
確保類別宣告所有介面 CompilerErrorProps
的屬性。public
、private
或 protected
如果我們對建構函式參數使用關鍵字 public
,則 TypeScript 會為我們執行兩件事
因此,以下兩個類別是等效的
class Point1 {constructor(public x: number, public y: number) {
}
}
class Point2 {: number;
x: number;
yconstructor(x: number, y: number) {
.x = x;
this.y = y;
this
} }
如果我們使用 private
或 protected
取代 public
,則對應的實例屬性為私有或受保護(非公開)。
在 TypeScript 中,兩個建構可以是抽象的
以下程式碼示範抽象類別和方法。
一方面,有抽象超類別 Printable
及其輔助類別 StringBuilder
class StringBuilder {= '';
string add(str: string) {
.string += str;
this
}
}abstract class Printable {
toString() {
= new StringBuilder();
const out .print(out);
this.string;
return out
}abstract print(out: StringBuilder): void;
}
另一方面,有具體子類別 Entries
和 Entry
class Entries extends Printable {: Entry[];
entriesconstructor(entries: Entry[]) {
super();
.entries = entries;
this
}print(out: StringBuilder): void {
for (const entry of this.entries) {
.print(out);
entry
}
}
}
class Entry extends Printable {: string;
key: string;
valueconstructor(key: string, value: string) {
super();
.key = key;
this.value = value;
this
}print(out: StringBuilder): void {
.add(this.key);
out.add(': ');
out.add(this.value);
out.add('\n');
out
} }
最後,這是我們使用 Entries
和 Entry
的方式
= new Entries([
const entries new Entry('accept-ranges', 'bytes'),
new Entry('content-length', '6518'),
;
]).equal(
assert.toString(),
entries'accept-ranges: bytes\ncontent-length: 6518\n');
關於抽象類別的注意事項