TS中的類系統對比起JS完善了許多,知識點包括但不限於可訪問性、繼承類、實現介面、訪問器、泛型、抽象類。 ...
TS官方手冊:TypeScript: Handbook - The TypeScript Handbook (typescriptlang.org)
類 Class
類的成員
初始化
類的成員屬性聲明類型:
class Point {
x: number;
y: number;
}
類的成員屬性初始化,會在實例化的時候完成賦值:
class Point {
x: number = 0;
y: number = 0;
}
嚴格初始化
--strictPropertyInitialization
配置項為true
的時候,要求成員屬性必須初始化,否則報錯。
可以在聲明成員屬性的時候初始化,也可以在構造函數中初始化。
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
如果打算在構造函數以外初始化欄位,例如依賴一個外部庫來填充類的一部分,則可以使用斷言運算符!
來聲明屬性是非空的。
class OKGreeter {
// 沒有初始化,但不會報錯
name!: string;
}
只讀 readonly
使用readonly
修飾,被readonly
修飾的成員只能在構造函數中被賦值(初始化),在其它成員方法中的更新操作會導致錯誤。
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
// name屬性是只讀的,這裡會導致報錯。
this.name = "not ok";
}
}
構造函數
-
參數列表的類型聲明;
-
參數的預設值;
-
構造函數重載:
class Point { // Overloads constructor(x: number, y: string); constructor(s: string); constructor(xs: any, y?: any) { // TBD } }
構造函數簽名與函數簽名之間的區別:
- 構造函數不能使用泛型;
- 構造函數不能聲明返回值類型。
成員方法
成員方法可以像函數一樣使用類型標註:參數列表的類型與預設值、返回值類型、泛型、重載......
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
註:在成員方法中使用成員屬性要通過this
,否則可能順著作用域鏈找到類外部的變數。
let x: number = 0;
class C {
x: string = "hello";
m() {
// 這裡的x是第1行的x,類型為number,不能賦值為string,故報錯。
x = "world";
}
}
訪問器 getter/setter
在 JS 中,如果沒有需要做數據攔截的需求,是不需要用訪問器的,大可以直接將屬性public
暴露到外部。
在 TS 中,訪問器存在如下規則:
- 如果有getter但沒有setter,那麼屬性是只讀的
readonly
; - 如果沒有指定setter方法的value參數類型,那麼則以getter的返回值類型替代;
- getter和setter的成員可訪問性(public/private/protected)必須一致。
索引簽名
可以為類的實例定義索引簽名,但是很少用,一般將索引數據轉移到別處,例如轉而使用一個對象類型或者數組類型的成員。
類的繼承
和其它面向對象語言一樣,JS 中的類可以從基類中繼承成員屬性和方法。
implements
子句(實現介面)
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
介面只負責聲明成員變數和方法,如果一個類要實現一個介面,則需要實現內部的所有方法。
一個類可以實現多個介面。
註意:
- 如果介面中聲明瞭函數的類型,在實現該介面的類中仍要聲明類型:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// 這裡的 s 會被認為是any類型,any類型沒有toLowerCase方法,會報錯
return s.toLowerCase() === "ok";
}
}
- 當一個類實現一個介面時,這個介面中的可選屬性(optional property)不會被待到類中。
extends
子句(繼承基類)
class A extends B{}
其中A被稱為子類或派生類,B是父類或基類。
繼承一個類將繼承它的所有成員屬性和方法。
方法重寫(overriding methods):
可以使用super
獲取到父類的方法。
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
可以將一個子類的實例賦值給一個父類的實例(實現多態的基礎)。
成員可訪問性 member visibility
public
預設值。使用public修飾的成員可以被任意訪問。
protected
只有這個類和它的子類的成員可以訪問。
子類在修飾繼承自父類的成員可訪問性時,最好帶上protected,否則會預設地變成public,將成員暴露給外部。
class Base {
protected m = 10;
}
class Derived extends Base {
// 沒有修飾,預設表示public
m = 15;
}
const d = new Derived();
console.log(d.m); // 暴露到外部了
跨繼承訪問protected成員
protected的定義就是只有類本身和子類可以訪問。但是在某些面向對象的編程語言中可以通過基類的引用,訪問到非本身且非子類的protected成員。
這種操作在 Java 中被允許,但是在C#、C++、TS 中是非法操作。
原則是:如果D2不是D1的子類,根據protected的定義這種訪問方式就是不合法的。那麼基類跨越這種技巧不能很好的解決問題。當在編碼的過程中遇到這種無法訪問的許可權問題時,應更多地思考類之間的結構設計,而不是採用這種取巧的方式。
class Base { protected x: number = 1; } class Derived1 extends Base { protected x: number = 5; } class Derived2 extends Base { f1(other: Derived2) { other.x = 10; } f2(other: Derived1) { // x被protected修飾,只能被Derived1的子類訪問,但是Derived2不是它的子類,無權訪問,會報錯。 other.x = 10; } }
private
只有類本身可以訪問。
與protected不同,protected在子類中可訪問,因此可以在子類中進一步開放可訪問性(即改為public)。
但是private修飾的成員無法在子類中訪問,因為無法進一步開放可訪問性。
跨實例訪問private成員
不同的實例只要是由一個類創建,那麼它們就可以相互訪問各自實例上由private修飾的成員。
class A { private x = 10; public sameAs(other: A) { // 不會報錯,因為TS支持跨實例訪問private成員 return other.x === this.x; } }
大多數面向對象語言支持這種特性,例如:
Java
,C#
,C++
,Swift
,PHP
;TS
也支持。Ruby
不支持。
註意事項
-
成員可訪問性只在TS的類型檢查過程中有效,在最終的 JS 運行時下是無效的,在 JS 運行時下,
in
操作符和其它獲取對象屬性的方法可以獲取到對象的所有屬性,不管在 TS 中它們是public還是protected還是private修飾的。 -
private
屬性支持使用obj.[key]
格式訪問,使得單元測試更加方便,但是這種訪問方式執行的是不嚴格的private
:class MySafe { private secretKey = 12345; } const s = new MySafe(); // 由private修飾的成員無法被訪問,這裡會報錯。 console.log(s.secretKey); // 使用字元串索引訪問,不嚴格,不會報錯。 console.log(s["secretKey"]);
靜態成員
基本特性
靜態成員綁定在類對象上,不需要實例化對象就能訪問。
靜態成員也可以通過public
,protected
,private
修飾可訪問性。
靜態成員也可以被繼承。
靜態成員不能取特殊的變數名,例如:
name
,length
,call
等等。不要使用
Function
原型上的屬性作為靜態成員的變數名,會因為衝突而出錯。
靜態代碼塊static block
靜態代碼塊中可以訪問到類內部的所有成員和類外部的內容,通常靜態代碼塊用來初始化類。
class Foo {
static #count = 0;
get count() {
return Foo.#count;
}
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
泛型類
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
泛型類的靜態成員不能引用類型參數。
this 在運行時的指向問題
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// 這裡會輸出"MyClass"
console.log(c.getName());
// 這裡輸出結果是"obj",而不是"MyClass",因為方法是通過obj調用的。
console.log(obj.getName());
類的方法內部的this
預設指向類的實例。但是一旦將方法挑出外部,單獨調用,就很可能報錯。因為函數中的this
指向調用該函數的對象,成員方法中的this
不一定指向它的實例對象,而是指向實際調用它的對象。
一種解決方法:使用箭頭函數。
箭頭函數中的this
指向取決於定義該箭頭函數時所處的上下文,而不是調用時。
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// 這裡會輸出"MyClass"
console.log(g());
註:
-
這種解決方案不需要 TS 也能實現;
-
這種做法會需要更多記憶體,因為箭頭函數不會被放到原型上,每個實例對象都有相互獨立的
getName
方法; -
也因為
getName
方法沒有在原型鏈上,在這個類的子類中,無法使用super.getName
訪問到getName
方法。
另一種解決方法:指定this
的類型
我們希望this
指向實例對象,意味著this
的類型應該是MyClass
而不能是其他,通過這種類型聲明可以在出錯的時候及時發現。
class MyClass {
name = "MyClass";
// 指定this必須是MyClass類型
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
const g = c.getName;
// Error: 這裡的this會指向undefined或者全局對象。
console.log(g());
註:
- 每個類定義分配一個函數,而不是每個類實例分配一個函數;
- 可以使用
super
調用,因為存在於原型鏈上。
this 類型
在類里存在一種特殊的類型this
,表示當前類。
返回值類型為this的情況:
class Box {
contents: string = "";
// set方法返回了this(這裡的this是對象的引用),因此set方法的返回值類型被推斷為this(這裡的this是類型)
set(value: string) {
this.contents = value;
return this;
}
}
參數類型為this的情況:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
這種情況下的other:this
與other:Box
不同,當一個類繼承自Box
時,子類中的sameAs
方法的this
類型將指向子類類型而不是Box
。
使用this進行類型守護(type guards)
可以在類或介面的方法的返回值類型處使用this is Type
,並搭配if
語句進行類型收束。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
// 這裡省略了子類的定義...
// 當需要類型收束時:
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile()) {
// 調用isFile方法將返回boolean類型,並且在這個塊內,fso的類型會收束為FileRep
fso.content;
} else if (fso.isDirectory()) {
fso.children;
} else if (fso.isNetworked()) {
fso.host;
}
另外一種常用的情景是:移除undefined
類型。
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
// (property) Box<unknown>.value?: unknown
box.value;
if (box.hasValue()) {
// (property) value: unknown
box.value;
}
參數屬性
由於構造函數的參數列表和成員屬性的屬性名大多數時候都是一致的:
class Box{
private width: number = 0;
private height: number = 0;
constructor(width: number, height:number){
this.width = width;
this.height = height;
}
}
TS 支持給類構造函數的參數添加修飾,例如public
,protected
,private
,readonly
。只需要在參數列表添加修飾就完成初始化操作,不需要寫構造函數的函數體:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// 不需要函數體
}
}
const a = new Params(1, 2, 3);
console.log(a.x); // 1
console.log(a.z); // Error: z是私有屬性,無法訪問
類表達式
類表達式和類的聲明十分相似,類表達式可以是匿名的,也可以將其賦值給任意標識符並引用它。
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world");
類表達式實際上是 JS 就有的語法,TS 只是提供了類型標註、泛型等額外的特性。
獲取實例類型
使用InstanceType
class Point {
createdAt: number;
x: number;
y: number;
constructor(x: number, y: number) {
this.createdAt = Date.now();
this.x = x;
this.y = y;
}
}
// 獲取Point這個類的實例類型
type PointInstance = InstanceType<typeof Point>
function moveRight(point: PointInstance) {
point.x += 5;
}
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
似乎這裡可以直接用
point:Point
替代point:PointInstance
,但是在其它沒有使用class(語法糖)的場景下,InstanceType
有以下作用: