本文 AI 產出,尚未審核

JavaScript 原型與繼承 – 靜態方法(static)

簡介

在 JavaScript 中,類別(class)其實是 語法糖,底層仍然是基於 prototype(原型)的機制。除了實例方法(instance method)之外,靜態方法(static method)是一種只能直接透過類別本身呼叫,而無法在實例上使用的功能。

靜態方法在設計 API、工具函式、或是需要在類別層級完成某些「一次性」工作時非常有用。正確掌握靜態方法的使用方式,能讓程式碼更具可讀性、可維護性,同時避免不必要的記憶體開銷。

本篇將從概念說明、實作範例、常見陷阱到最佳實踐,逐步帶你了解 static 在原型與繼承中的角色,並提供實務上可直接套用的範例。


核心概念

1. 什麼是靜態方法?

  • 定義:在 class 宣告前加上 static 關鍵字的方法。
  • 呼叫方式:只能以 ClassName.method() 形式呼叫,不能instance.method() 呼叫。
  • 存放位置:靜態方法直接掛在建構函式(constructor)上,而不是掛在建構函式的 prototype 上。
class MyClass {
  static greet() {
    console.log('Hello from static method!');
  }
}
MyClass.greet();          // 正確
// const obj = new MyClass();
// obj.greet();            // TypeError: obj.greet is not a function

2. 為什麼要使用 static?

使用情境 靜態方法的好處
工具函式(如 Array.isArrayObject.assign 不需要建立實例,直接提供功能,降低記憶體使用。
工廠方法(Factory) 依據參數回傳不同子類別的實例,將建立邏輯集中於類別本身。
常數或設定 static 屬性保存常量,讓程式碼更具語意。
繼承時共享行為 子類別自動繼承父類別的靜態方法,方便擴充。

3. 靜態屬性 vs 靜態方法

ES2022 引入了 靜態屬性(static fields),語法與靜態方法類似,但用於儲存值而非函式。

class Config {
  static version = '1.0.0';      // 靜態屬性
  static getVersion() {         // 靜態方法
    return Config.version;
  }
}
console.log(Config.getVersion()); // 1.0.0

4. 繼承中的 static

子類別會自動繼承父類別的靜態方法與屬性,但 super 只能在靜態方法內使用 super 來呼叫父類別的靜態成員。

class Animal {
  static kingdom = 'Animalia';
  static describe() {
    return `All animals belong to ${this.kingdom}.`;
  }
}
class Dog extends Animal {
  static kingdom = 'Canidae';   // 覆寫
}
console.log(Dog.describe());   // All animals belong to Canidae.

程式碼範例

範例 1️⃣:簡易工具類別 – MathUtil

提供常用的數學運算工具,全部以 static 方法實作。

class MathUtil {
  // 回傳兩數的最大公因數
  static gcd(a, b) {
    while (b !== 0) {
      [a, b] = [b, a % b];
    }
    return Math.abs(a);
  }

  // 判斷是否為質數
  static isPrime(num) {
    if (num <= 1) return false;
    if (num <= 3) return true;
    if (num % 2 === 0 || num % 3 === 0) return false;
    for (let i = 5; i * i <= num; i += 6) {
      if (num % i === 0 || num % (i + 2) === 0) return false;
    }
    return true;
  }

  // 計算階乘(使用遞迴)
  static factorial(n) {
    if (n < 0) throw new Error('Negative number not allowed');
    return n <= 1 ? 1 : n * MathUtil.factorial(n - 1);
  }
}

// 使用方式
console.log(MathUtil.gcd(48, 18));      // 6
console.log(MathUtil.isPrime(17));     // true
console.log(MathUtil.factorial(5));    // 120

重點MathUtil 不需要實例化,直接以類別呼叫即可,避免不必要的記憶體開銷。


範例 2️⃣:工廠方法 – ShapeFactory

根據參數回傳不同子類別(CircleRectangle)的實例。

class Shape {
  // 只提供抽象的描繪方法
  draw() {
    throw new Error('draw() must be implemented by subclass');
  }

  // 靜態工廠方法
  static create(type, options) {
    switch (type) {
      case 'circle':
        return new Circle(options.radius);
      case 'rectangle':
        return new Rectangle(options.width, options.height);
      default:
        throw new Error(`Unknown shape type: ${type}`);
    }
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  draw() {
    console.log(`Drawing a circle with radius ${this.radius}`);
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  draw() {
    console.log(`Drawing a rectangle ${this.width}×${this.height}`);
  }
}

// 呼叫工廠方法
const c = Shape.create('circle', { radius: 10 });
const r = Shape.create('rectangle', { width: 5, height: 8 });

c.draw();   // Drawing a circle with radius 10
r.draw();   // Drawing a rectangle 5×8

說明Shape.create靜態工廠方法,負責根據需求產生正確的子類別實例,讓呼叫端不必關心具體的建構細節。


範例 3️⃣:靜態屬性與繼承 – Logger

使用靜態屬性保存全域設定,子類別可自行覆寫或擴充。

class Logger {
  static level = 'info';               // 預設日誌等級
  static prefix = '[APP]';             // 前綴字串

  static log(message, level = this.level) {
    if (this.shouldLog(level)) {
      console.log(`${this.prefix} ${level.toUpperCase()}: ${message}`);
    }
  }

  static shouldLog(level) {
    const levels = ['debug', 'info', 'warn', 'error'];
    return levels.indexOf(level) >= levels.indexOf(this.level);
  }
}

// 子類別想要改變前綴與等級
class AuthLogger extends Logger {
  static prefix = '[AUTH]';
  static level = 'debug';
}

// 呼叫
Logger.log('Server started');                 // [APP] INFO: Server started
AuthLogger.log('User login success');         // [AUTH] DEBUG: User login success
AuthLogger.log('Invalid token', 'warn');      // [AUTH] WARN: Invalid token

關鍵this 在靜態方法內會指向呼叫該方法的類別(LoggerAuthLogger),因此子類別自動繼承並可覆寫設定。


範例 4️⃣:使用 static 產生唯一 ID(Singleton Pattern)

利用靜態屬性保存唯一實例,確保全域只會有一個物件。

class IdGenerator {
  static #instance = null;   // 私有靜態欄位
  static #counter = 0;

  constructor() {
    if (IdGenerator.#instance) {
      return IdGenerator.#instance;
    }
    IdGenerator.#instance = this;
  }

  static next() {
    return ++IdGenerator.#counter;
  }
}

// 直接使用 static 方法產生 ID
console.log(IdGenerator.next()); // 1
console.log(IdGenerator.next()); // 2

// 嘗試建立實例也不會影響靜態行為
const gen = new IdGenerator();
console.log(gen instanceof IdGenerator); // true
console.log(IdGenerator.next()); // 3

說明#instance 為私有靜態欄位,配合 static next() 實作簡易的 Singleton,確保 ID 產生器全域唯一。


範例 5️⃣:在 ES6 之前的「模擬」static(使用函式與 prototype)

了解舊版 JavaScript 如何手動掛載靜態方法,對比現代語法。

function OldMath() {}

// 手動掛在建構函式上
OldMath.max = function (a, b) {
  return a > b ? a : b;
};

// 原型上仍可放實例方法
OldMath.prototype.add = function (a, b) {
  return a + b;
};

console.log(OldMath.max(5, 9));               // 9
const o = new OldMath();
console.log(o.add(3, 4));                     // 7

重點:ES6 以前的寫法與 class 的 static 本質相同,只是語法較為繁瑣。了解這點有助於閱讀舊有程式碼。


常見陷阱與最佳實踐

陷阱 說明 改善方式
把實例方法寫成 static 靜態方法無法存取 this(指向實例),會導致錯誤的設計。 只在不需要實例屬性的情況下使用 static;若需要 this,改用 prototype 方法。
忘記 super 呼叫父類別的 static 方法 子類別覆寫 static 後,若仍需父類別行為,必須手動 super.method() 在子類別的 static 方法內使用 super.method(),或在子類別外部直接呼叫父類別。
過度使用 static 把太多功能塞進 static,會讓類別變成「工具箱」而失去單一職責。 依照 SRP(單一職責原則) 拆分工具類別;僅在「全域」或「工廠」性質的功能使用 static。
靜態屬性被意外改寫 靜態屬性是公有的,外部可以直接改寫,可能破壞預期行為。 使用 私有靜態欄位static #field)或凍結(Object.freeze) 來保護重要設定。
在靜態方法中使用 arrow function 失去 this 箭頭函式不會自行綁定 this,會導致 this 指向外層作用域。 若需使用 this(指向類別本身),直接使用普通函式或在箭頭函式外部先儲存 const cls = this;

最佳實踐

  1. 命名慣例:使用大寫開頭的類別名稱,靜態方法則使用動詞或描述性名稱(如 create, fromJSON, isValid)。
  2. 保持純函式:靜態方法若不依賴外部狀態,應保持 純函式(pure function),提升可測試性。
  3. 文件化:在 JSDoc 中標註 @static,讓 IDE 能正確提示。
  4. 測試覆蓋:即使是工具函式,也要寫單元測試,確保行為不因重構而變異。
  5. 與 prototype 區分:明確區分「實例行為」與「類別行為」,避免混淆。

實際應用場景

場景 為何適合使用 static 範例
API 客戶端(如 axios 請求工具不需要保存實例狀態,且常以 Axios.get() 形式呼叫。 Axios.get(url).then(...);
資料模型的序列化/反序列化 Model.fromJSON(json) 能在不建立實例的情況下產生物件。 User.fromJSON(data)
驗證器(Validator) 常見的正則驗證不依賴實例,可寫成 Validator.isEmail(str) if (Validator.isEmail(email)) …
全域設定或常數 Config.BASE_URLConfig.TIMEOUT 只需一份,使用 static 屬性即可。 fetch(Config.BASE_URL + '/users')
工廠模式 依據參數產生不同類別的實例,集中在一個入口點。 ShapeFactory.create('circle', {...})
日誌系統 需要根據環境切換等級,使用 static 屬性與方法管理全域日誌。 Logger.log('Message')

總結

  • static 是 JavaScript 類別(class)中,用於定義只能在類別本身呼叫的 方法屬性
  • 靜態方法不會掛在 prototype 上,因而不會被實例繼承,但 子類別仍會繼承 父類別的 static 成員,並可使用 super 呼叫。
  • 常見的應用包括 工具函式、工廠方法、全域設定、日誌系統 等,使用 static 可以減少不必要的實例化與記憶體開銷。
  • 需要注意的陷阱有 誤用 this、過度集中功能、屬性被意外改寫,遵循 單一職責、純函式、文件化 的原則,可寫出可維護且易測試的程式碼。

掌握靜態方法的正確使用方式,將讓你的 JavaScript 程式設計更具彈性與可讀性,亦能在大型專案中保持代碼結構的清晰與一致。祝你在原型與繼承的世界裡,寫出更優雅的程式!