TypeScript 課程 – 類別(Classes)
主題:static 成員
簡介
在物件導向程式設計中,類別(class)是用來描述 「什麼」 以及 「如何」 建立物件的藍圖。大多數時候,我們會把屬性與方法寫在類別的實例(instance)上,讓每一個物件各自擁有自己的資料與行為。然而,當某些資訊或功能 與特定實例無關,而是 屬於整個類別本身 時,使用 static 成員就能讓程式碼更具可讀性與效能。
static 成員在 TypeScript(以及 JavaScript)中扮演了類似「全域」的角色——它們屬於類別本身,而不是類別的實例。了解什麼時候該使用 static、如何正確宣告與存取,對於寫出可維護、可重用的程式碼相當重要。接下來,我們會一步步拆解 static 成員的概念,並透過實務範例說明其應用方式。
核心概念
1. static 成員的基本語法
在 TypeScript 中,只要在屬性或方法前加上 static 關鍵字,即可將它定義為類別的靜態成員。以下是一個最簡單的範例:
class MathUtil {
// 靜態屬性
static PI: number = 3.14159;
// 靜態方法
static circleArea(radius: number): number {
return MathUtil.PI * radius * radius;
}
}
// 直接透過類別名稱存取
console.log(MathUtil.PI); // 3.14159
console.log(MathUtil.circleArea(5)); // 78.53975
重點:
static成員只能透過 類別本身(如MathUtil.PI)存取,不能 透過實例(如new MathUtil().PI)取得。
2. 為什麼要使用 static?
| 場景 | 使用 static 的好處 |
|---|---|
常數值(如 PI、MAX_SIZE) |
只需要一份資料,避免每個實例都複製同樣的值 |
| 工具函式(Utility functions) | 不需要建立實例即可直接呼叫,提高使用便利性 |
| 共享資源(如快取、計數器) | 所有實例共用同一個狀態,方便統計或協調 |
| 設計模式(Singleton、Factory) | 可在類別內部維持唯一實例或建立物件的工廠方法 |
3. 靜態屬性與實例屬性的差異
class Counter {
// 靜態屬性:全域計數器
static totalCount: number = 0;
// 實例屬性:每個 Counter 物件自己的計數
private current: number = 0;
constructor() {
Counter.totalCount++; // 每建立一個實例,總計數加一
}
increment(): void {
this.current++;
}
getCurrent(): number {
return this.current;
}
static getTotalCount(): number {
return Counter.totalCount;
}
}
const a = new Counter();
const b = new Counter();
a.increment(); a.increment();
b.increment();
console.log(a.getCurrent()); // 2
console.log(b.getCurrent()); // 1
console.log(Counter.getTotalCount()); // 2(兩個實例總數)
totalCount為 類別層級 的資料,所有Counter實例共享同一個值。current為 實例層級 的資料,每個物件各自持有自己的計數。
4. 靜態方法內部的 this 指向
在靜態方法中,this 會指向 類別本身,而不是實例。這讓我們可以在靜態方法裡直接操作其他靜態成員:
class Logger {
private static prefix: string = '[Log]';
static setPrefix(p: string): void {
this.prefix = p; // `this` 代表 Logger 類別
}
static info(message: string): void {
console.log(`${this.prefix} ${message}`);
}
}
Logger.info('程式開始'); // [Log] 程式開始
Logger.setPrefix('[Info]'); // 改變前綴
Logger.info('初始化完成'); // [Info] 初始化完成
注意:若在靜態方法裡使用
new this(),則會建立 同類別 的新實例。
5. 靜態區塊(Static Initialization Block) – ES2022 之後的特性
TypeScript 4.4 起支援 靜態初始化區塊,讓我們可以在類別定義時執行一次性的初始化程式碼:
class Config {
static readonly API_URL: string;
static readonly TIMEOUT: number;
// 靜態區塊只會執行一次
static {
const env = process.env.NODE_ENV ?? 'development';
if (env === 'production') {
Config.API_URL = 'https://api.example.com';
Config.TIMEOUT = 5000;
} else {
Config.API_URL = 'http://localhost:3000';
Config.TIMEOUT = 10000;
}
}
}
console.log(Config.API_URL); // 依環境輸出不同的 URL
6. static 與繼承(Inheritance)
子類別可以 繼承 父類別的靜態成員,且也可以 覆寫(shadow)它們:
class Animal {
static kingdom: string = 'Animalia';
static describe(): string {
return `All animals belong to ${this.kingdom}`;
}
}
class Bird extends Animal {
// 覆寫父類別的靜態屬性
static kingdom = 'Aves';
}
console.log(Animal.describe()); // All animals belong to Animalia
console.log(Bird.describe()); // All animals belong to Aves
this在靜態方法裡會根據 實際呼叫的類別 解析(稱為 polymorphic this),因此Bird.describe()會使用Bird.kingdom。
程式碼範例(實用示例)
範例 1️⃣:全域唯一 ID 產生器
class IdGenerator {
private static lastId: number = 0;
static next(): number {
return ++this.lastId; // 使用 this 以支援子類別覆寫
}
}
// 任何地方都可以取得唯一編號
console.log(IdGenerator.next()); // 1
console.log(IdGenerator.next()); // 2
範例 2️⃣:簡易快取(Cache)實作
class SimpleCache<T> {
private static store: Map<string, any> = new Map();
static set(key: string, value: T): void {
this.store.set(key, value);
}
static get<T>(key: string): T | undefined {
return this.store.get(key) as T | undefined;
}
static clear(): void {
this.store.clear();
}
}
// 使用快取
SimpleCache.set('user_1', { name: 'Alice', age: 30 });
const user = SimpleCache.get<{ name: string; age: number }>('user_1');
console.log(user?.name); // Alice
範例 3️⃣:工廠模式(Factory Pattern)結合 static
interface Shape {
draw(): void;
}
class Circle implements Shape {
draw() { console.log('畫圓形'); }
}
class Square implements Shape {
draw() { console.log('畫方形'); }
}
class ShapeFactory {
// 靜態方法返回不同的 Shape 物件
static create(type: 'circle' | 'square'): Shape {
switch (type) {
case 'circle': return new Circle();
case 'square': return new Square();
default: throw new Error('未知的形狀類型');
}
}
}
// 呼叫工廠,不需要實例化 ShapeFactory
ShapeFactory.create('circle').draw(); // 畫圓形
ShapeFactory.create('square').draw(); // 畫方形
範例 4️⃣:單例(Singleton)實作
class Database {
private static _instance: Database | null = null;
private constructor(public readonly connectionString: string) {}
static get instance(): Database {
if (!this._instance) {
// 只會在第一次存取時建立
this._instance = new Database('Server=127.0.0.1;DB=test;');
}
return this._instance;
}
query(sql: string): void {
console.log(`執行查詢: ${sql}`);
}
}
// 任何地方都能取得同一個實例
Database.instance.query('SELECT * FROM users');
範例 5️⃣:使用靜態區塊載入環境設定
class EnvConfig {
static readonly isProd: boolean;
static readonly apiEndpoint: string;
static {
const env = process.env.NODE_ENV ?? 'development';
this.isProd = env === 'production';
this.apiEndpoint = this.isProd
? 'https://api.prod.example.com'
: 'https://api.dev.example.com';
}
}
console.log(EnvConfig.apiEndpoint);
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 / 最佳實踐 |
|---|---|---|
誤以為 static 成員可以被實例存取 |
instance.prop = Class.prop 會失效,且 TypeScript 會報錯 |
只在類別本身 使用 Class.prop,若需要在實例裡使用,可在建構子裡把值複製到實例屬性 |
| 靜態屬性未初始化 | 直接宣告 static foo: number; 會得到 undefined,可能導致運算錯誤 |
為靜態屬性提供預設值或在 靜態區塊 中明確初始化 |
在靜態方法裡使用 this 產生循環依賴 |
static foo() { return new this(); } 可能在子類別覆寫時產生不預期的行為 |
只在確定不會被覆寫的情況下使用 new this(),或改用具體類別名稱 |
過度使用 static 造成測試困難 |
靜態狀態會在測試間共享,導致測試互相影響 | 依賴注入 或在測試前手動重設靜態屬性(如 Class.prop = initialValue) |
| 靜態屬性與實例屬性同名 | 會造成混淆,尤其在 IDE 裡自動完成時 | 為靜態屬性加上前綴或使用不同命名風格(例如 STATIC_) |
最佳實踐總結:
- 僅在真的是類別層級的資訊 時使用
static(常數、工具函式、共享快取)。 - 保持靜態狀態可預測:如果靜態屬性會改變,提供 重設方法 或在測試前手動清除。
- 善用
readonly讓不可變的靜態常數在編譯期即受保護。 - 使用靜態區塊 處理一次性設定,避免在每次建構子裡重複執行。
- 在繼承樹中,注意子類別會繼承靜態屬性,必要時使用 覆寫(shadow)或
super取得父類的靜態成員。
實際應用場景
| 場景 | 為何選擇 static |
範例說明 |
|---|---|---|
| 全域設定檔(如 API URL、環境變數) | 只需要一組設定,且在程式執行期間保持不變 | Config.API_URL、Config.isProd |
| 唯一編號產生(如訂單編號、訊息 ID) | 必須在所有實例間共享遞增計數 | IdGenerator.next() |
| 快取與記憶體儲存(如圖片快取、資料庫連線池) | 多個物件需要共享同一份快取,避免重複載入 | ImageCache.get(url) |
| 工廠或建構器(Factory Pattern) | 建立物件的邏輯不屬於任何單一實例 | ShapeFactory.create('circle') |
| 單例模式(Singleton) | 保證全程只會有一個實例(例如資料庫連線) | Database.instance |
| 統計與監控(如計算方法呼叫次數) | 所有實例共同累計資料 | Counter.totalCount |
總結
static 成員是 TypeScript 類別中 「類別層級」 的資源與行為,它讓我們可以:
- 集中管理常數與工具函式,不必為每個實例重複建立。
- 共享狀態(如快取、計數器),提升記憶體使用效能。
- 實作設計模式(Factory、Singleton、Builder 等),使程式結構更清晰。
- 利用靜態區塊 進行一次性的初始化,讓程式在不同環境下自動調整設定。
在實務開發中,適度使用 static 能減少不必要的物件產生、提升程式可讀性與維護性。但同時也要注意 避免過度依賴全域狀態,以免造成測試困難或意外的副作用。遵循上述最佳實踐,您就能在 TypeScript 專案中安全、有效地運用 static 成員,寫出更乾淨、更具擴充性的程式碼。祝您寫程式愉快! 🚀