本文 AI 產出,尚未審核

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 的好處
常數值(如 PIMAX_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_

最佳實踐總結

  1. 僅在真的是類別層級的資訊 時使用 static(常數、工具函式、共享快取)。
  2. 保持靜態狀態可預測:如果靜態屬性會改變,提供 重設方法 或在測試前手動清除。
  3. 善用 readonly 讓不可變的靜態常數在編譯期即受保護。
  4. 使用靜態區塊 處理一次性設定,避免在每次建構子裡重複執行。
  5. 在繼承樹中,注意子類別會繼承靜態屬性,必要時使用 覆寫(shadow)或 super 取得父類的靜態成員。

實際應用場景

場景 為何選擇 static 範例說明
全域設定檔(如 API URL、環境變數) 只需要一組設定,且在程式執行期間保持不變 Config.API_URLConfig.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 成員,寫出更乾淨、更具擴充性的程式碼。祝您寫程式愉快! 🚀