[TS手冊學習] 04_類
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有以下作用:![]()
??引用自javascript - typescript的InstanceType怎么用呀? - SegmentFault 思否
抽象類與成員
TS 中的類和成員可以是抽象的。
抽象類與成員的特點:
- 抽象方法或屬性是指尚未提供實現的方法或屬性。
- 這些抽象成員必須存在于抽象類中。
- 抽象類不能直接實例化。
抽象類必須充當基類,讓派生類去實現抽象成員。
當一個類不存在任何抽象成員的時候,就說它是具體類。
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
// 繼承
class Derived extends Base {
// 實現基類的抽象成員
getName() {
return "world";
}
}
如果沒有實現基類的抽象成員,則會報錯。
類之間的關系
TS 通過類的結構區分不同的類,如果兩個類的結構一致,則認為是同一個類。
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
只要一個類的字段集合是另一個類的字段集合的子集,就可以實現類似于繼承的效果(不需要使用extends關鍵字)。
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
更極端的情況:空集!
class Empty {}
function fn(x: Empty) {
// x對象沒有字段,這里做不了任何操作
}
// 但是下面這些對象的字段集合都包含空集,也就是說都不會報錯。
fn(window);
fn({});
fn(fn);
永遠不要使用空的類。

浙公網安備 33010602011771號