在 TypeScript (TS) 中,類是物件導向程式設計的核心概念之一。類是用來建立物件的模板,它封裝了物件的狀態(屬性)和行為(方法)。TypeScript 提供了對類的全面支援,並且還增加了型別檢查功能,使得程式碼更加嚴謹和易於維護。
類的本質
類是物件導向程式設計(OOP)中的核心概念,它提供了建立物件的模板。在 TypeScript 中,類是用來封裝資料(屬性)和操作這些資料的方法的。
class Person { name: string; // 定義屬性 age: number; // 建構函式,用於初始化類的屬性 constructor(name: string, age: number) { this.name = name; this.age = age; } // 定義方法 greet() { console.log(`你好,我的名字是 ${this.name},我今年 ${this.age} 歲了。`); } } // 例項化類 const person1 = new Person("Alice", 30); person1.greet(); // 輸出: 你好,我的名字是 Alice,我今年 30 歲了。
在上面的程式碼中有如下解釋:
屬性:name 和 age 是類的屬性,它們儲存了每個例項的狀態(資訊)。
建構函式:constructor 是類的特殊方法,用於在建立物件時初始化屬性。
方法:greet() 是類的方法,用於表示物件的行為。
最終結果如下圖所示:
靜態成員與非靜態成員的區別
在 TypeScript 中,類的成員可以分為靜態成員和非靜態成員。它們的主要區別在於:
非靜態成員:屬於每個例項物件。每建立一個新的物件,都會建立一組獨立的非靜態成員。
靜態成員:屬於類本身,而不是例項。靜態成員可以透過類名直接訪問,不需要例項化物件。
如下程式碼所示:
class Car { static totalCars = 0; // 靜態變數,屬於類本身 mileage: number; // 非靜態變數,屬於每個物件 constructor(mileage: number) { this.mileage = mileage; Car.totalCars++; // 更新靜態變數 } // 靜態方法 static showTotalCars() { console.log(`總共有 ${Car.totalCars} 輛車。`); } // 非靜態方法 showMileage() { console.log(`這輛車的里程是 ${this.mileage} 公里。`); } } const car1 = new Car(10000); const car2 = new Car(20000); Car.showTotalCars(); // 輸出: 總共有 2 輛車。 car1.showMileage(); // 輸出: 這輛車的里程是 10000 公里。
在上面的程式碼中有如下解釋:
靜態變數 totalCars 是一個類級別的屬性,所有 Car 物件共享這個值。
非靜態變數 mileage 是每個 Car 物件獨立的屬性,建立一個新物件就會有一個獨立的 mileage 值。
靜態方法 showTotalCars 是透過類名呼叫的,而不是透過物件呼叫的。
最終輸出結果如下圖所示:
為什麼靜態成員不能訪問非靜態成員?
由於靜態成員屬於類本身,而非靜態成員屬於例項。靜態成員和非靜態成員的生命週期不同。靜態成員在類載入時已經存在,而非靜態成員依賴於具體的例項物件。
class Example { static staticMember = "靜態成員"; nonStaticMember = "非靜態成員"; static staticMethod() { console.log(this.staticMember); // 這是正確的,因為 staticMember 是靜態的 // console.log(this.nonStaticMember); // 錯誤!靜態方法無法訪問非靜態成員 } } Example.staticMethod(); // 輸出: 靜態成員
靜態方法和屬性只與類關聯,而非靜態屬性和方法與物件例項關聯。靜態方法中沒有 this 指向具體的例項,因此無法訪問屬於某個物件的非靜態屬性或方法。透過類直接呼叫靜態方法時,類內部沒有任何關於例項的上下文。
如何在靜態方法中訪問非靜態成員?
如果要在靜態方法中訪問非靜態成員,可以透過將例項物件作為引數傳遞給靜態方法,進而訪問非靜態成員。
class Car { mileage: number; // 非靜態成員 static totalCars = 0; // 靜態成員 constructor(mileage: number) { this.mileage = mileage; Car.totalCars++; // 統計建立的車輛數 } static showCarMileage(car: Car) { console.log(`這輛車的里程是 ${car.mileage} 公里。`); // 透過傳遞的例項物件訪問非靜態成員 } } const car1 = new Car(15000); Car.showCarMileage(car1); // 輸出: 這輛車的里程是 15000 公里。
訪問修飾符(public, private, protected)
TypeScript 中的訪問修飾符用於控制類成員的訪問許可權:
public:公有成員,可以在類的外部訪問(預設修飾符)。
private:私有成員,只能在類的內部訪問,不能在外部或者子類中訪問。
protected:受保護的成員,可以在類的內部和子類中訪問,但不能在類的外部訪問。
class Person { public name: string; // 公有屬性 private age: number; // 私有屬性 protected id: number; // 受保護屬性 constructor(name: string, age: number, id: number) { this.name = name; this.age = age; this.id = id; } public greet() { console.log(`你好,我的名字是 ${this.name}`); } private showAge() { console.log(`我的年齡是 ${this.age}`); } protected showId() { console.log(`我的 ID 是 ${this.id}`); } } const person1 = new Person("Alice", 30, 123); // person1.age; // 錯誤,age 是私有屬性,無法在外部訪問 person1.greet(); // 輸出: 你好,我的名字是 Alice
訪問修飾符用於控制類的外部對類內部實現的訪問,從而實現資訊隱藏,這有助於保護類的內部狀態免於隨意修改。繼承中,protected 成員可以在子類中訪問,從而在繼承鏈中共享部分邏輯,而 private 成員則完全封閉。
繼承與方法重寫
TypeScript 支援類的繼承,透過 extends 關鍵字可以繼承父類的屬性和方法。繼承允許子類擴充套件父類的功能,並且可以重寫父類的方法。
class Animal { name: string; constructor(name: string) { this.name = name; } public makeSound(): void { console.log(`${this.name} makes a sound.`); } } class Dog extends Animal { constructor(name: string) { super(name); // 呼叫父類建構函式 } // 重寫父類的方法 public makeSound(): void { console.log(`${this.name} barks.`); } } const dog = new Dog("Buddy"); dog.makeSound(); // 輸出: Buddy barks.
在子類的建構函式中,super 用於呼叫父類的建構函式,從而初始化父類的屬性。子類還可以重寫父類的方法來改變行為。
抽象類和抽象方法
抽象類是不能被例項化的類,它們通常用作基類,定義子類必須實現的抽象方法。抽象方法在抽象類中沒有具體實現,必須由子類提供具體實現。
abstract class Animal { abstract makeSound(): void; // 抽象方法 public move(): void { console.log("The animal moves."); } } class Dog extends Animal { public makeSound(): void { console.log("Woof! Woof!"); } } const dog = new Dog(); dog.makeSound(); // 輸出: Woof! Woof! dog.move(); // 輸出: The animal moves.
Animal 是抽象類,不能直接例項化。抽象類用於提供通用功能,併爲子類定義行爲規範。makeSound 是抽象方法,沒有實現,子類必須提供具體實現。
介面(Interfaces)
介面定義了一組類必須實現的規範。介面只定義方法的簽名,而不提供方法的具體實現。
class Person { private _name: string; constructor(name: string) { this._name = name; } get name(): string { return this._name; } set name(newName: string) { if (newName.length > 0) { this._name = newName; } else { console.log("Name cannot be empty."); } } } const person = new Person("Alice"); console.log(person.name); // 獲取 name,輸出: Alice person.name = "Bob"; // 設定 name console.log(person.name); // 輸出: Bob
Drivable 是介面,定義了 drive 方法的簽名。Car 類透過 implements 實現了 Drivable 介面,並提供了 drive 方法的具體實現。
抽象類 vs 介面
抽象類和介面的基本概念:
抽象類:
抽象類是不能被直接例項化的類,只能作為基類被繼承。
抽象類可以包含抽象方法和具體方法。抽象方法沒有實現,必須由子類實現;具體方法可以在抽象類中有實現,子類可以繼承這些方法。
抽象類用於定義通用的行爲規範,同時可以提供部分實現,適合用於構建類的層次結構。
介面
介面是用來定義類的行爲規範的,它只包含方法和屬性的簽名,不包含具體實現。
類可以透過 implements 關鍵字實現介面,必須提供介面中定義的所有方法和屬性的實現。
介面主要用於型別檢查,並且允許一個類實現多個介面,實現類似於多繼承的效果。
抽象類可以包含建構函式、欄位(屬性)、具體方法、抽象方法,並且抽象方法必須由子類實現,具體方法可以由子類繼承和使用。
abstract class Animal { protected name: string; constructor(name: string) { this.name = name; } // 抽象方法,沒有實現 abstract makeSound(): void; // 具體方法,有實現 public move(): void { console.log(`${this.name} 正在移動。`); } } class Dog extends Animal { constructor(name: string) { super(name); } public makeSound(): void { console.log(`${this.name} 汪汪叫!`); } } const dog = new Dog("小黑"); dog.makeSound(); // 輸出: 小黑 汪汪叫! dog.move(); // 輸出: 小黑 正在移動。
介面只能定義方法和屬性的簽名,不包含任何實現,而且一個類可以實現多個介面,每個介面可以定義類的不同方面的行為。
interface Drivable { drive(): void; } interface Flyable { fly(): void; } class Plane implements Drivable, Flyable { public drive(): void { console.log("飛機在滑行。"); } public fly(): void { console.log("飛機在飛行。"); } } const plane = new Plane(); plane.drive(); // 輸出: 飛機在滑行。 plane.fly(); // 輸出: 飛機在飛行。
抽象類可以包含訪問修飾符(public、protected、private),用於控制成員的可見性和訪問許可權。而介面中的成員預設都是 public,不能使用 private 或 protected。
// 抽象類示例 abstract class Animal { protected name: string; constructor(name: string) { this.name = name; } abstract makeSound(): void; protected sleep(): void { console.log(`${this.name} 正在睡覺。`); } } class Cat extends Animal { constructor(name: string) { super(name); } public makeSound(): void { console.log(`${this.name} 喵喵叫!`); } public rest(): void { this.sleep(); // 可以訪問受保護的成員 } } const cat = new Cat("小花"); cat.makeSound(); // 輸出: 小花 喵喵叫! cat.rest(); // 輸出: 小花 正在睡覺。 // 介面示例 interface Swimmable { swim(): void; } // 不能在介面中使用訪問修飾符 interface Flyable { fly(): void; } class Fish implements Swimmable { public swim(): void { console.log("魚在游泳。"); } }
抽象類可以包含欄位(屬性)定義,並且可以有預設值。而介面不能包含欄位,只能定義方法簽名和屬性型別。
// 抽象類可以包含屬性 abstract class Vehicle { protected speed: number = 0; constructor(speed: number) { this.speed = speed; } abstract accelerate(amount: number): void; } class Car extends Vehicle { public accelerate(amount: number): void { this.speed += amount; console.log(`汽車的速度增加到 ${this.speed} km/h。`); } } const car = new Car(50); car.accelerate(20); // 輸出: 汽車的速度增加到 70 km/h。 // 介面不能包含屬性 interface Drivable { drive(): void; }
因此,如果一個類中有一些通用方法,可以透過抽象類提供部分實現,子類繼承這些方法。介面適合用於定義一組行爲規範,確保實現該介面的類提供特定的功能。
在實際開發中,抽象類和介面可以結合使用,充分利用它們各自的優勢。
// 定義介面 interface Swimmable { swim(): void; } interface Flyable { fly(): void; } // 抽象類 abstract class Animal { protected name: string; constructor(name: string) { this.name = name; } abstract makeSound(): void; public move(): void { console.log(`${this.name} 正在移動。`); } } // 具體類實現多個介面並繼承抽象類 class Duck extends Animal implements Swimmable, Flyable { constructor(name: string) { super(name); } public makeSound(): void { console.log(`${this.name} 呱呱叫!`); } public swim(): void { console.log(`${this.name} 正在游泳。`); } public fly(): void { console.log(`${this.name} 正在飛行。`); } } const duck = new Duck("小鴨子"); duck.makeSound(); // 輸出: 小鴨子 呱呱叫! duck.move(); // 輸出: 小鴨子 正在移動。 duck.swim(); // 輸出: 小鴨子 正在游泳。 duck.fly(); // 輸出: 小鴨子 正在飛行。
Duck 類繼承了 Animal 抽象類,並實現了 Swimmable 和 Flyable 介面。抽象類提供通用的行為(如 move),而介面提供具體的行爲規範(如 swim 和 fly),使得程式碼設計更加靈活和清晰。
總結
TypeScript 的類是物件導向程式設計的核心,封裝了物件的狀態(屬性)和行為(方法),支援靜態成員和非靜態成員的區別。類可以使用訪問修飾符(public
、private
、protected
)控制成員的可見性,並支援繼承和方法重寫來擴充套件父類的功能。抽象類和介面定義了行爲規範,其中抽象類可提供部分實現,而介面則用於型別約定,允許一個類實現多個介面,實現靈活的程式碼設計。