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.isArray、Object.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
根據參數回傳不同子類別(Circle、Rectangle)的實例。
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在靜態方法內會指向呼叫該方法的類別(Logger或AuthLogger),因此子類別自動繼承並可覆寫設定。
範例 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;。 |
最佳實踐
- 命名慣例:使用大寫開頭的類別名稱,靜態方法則使用動詞或描述性名稱(如
create,fromJSON,isValid)。 - 保持純函式:靜態方法若不依賴外部狀態,應保持 純函式(pure function),提升可測試性。
- 文件化:在 JSDoc 中標註
@static,讓 IDE 能正確提示。 - 測試覆蓋:即使是工具函式,也要寫單元測試,確保行為不因重構而變異。
- 與 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_URL、Config.TIMEOUT 只需一份,使用 static 屬性即可。 |
fetch(Config.BASE_URL + '/users') |
| 工廠模式 | 依據參數產生不同類別的實例,集中在一個入口點。 | ShapeFactory.create('circle', {...}) |
| 日誌系統 | 需要根據環境切換等級,使用 static 屬性與方法管理全域日誌。 | Logger.log('Message') |
總結
- static 是 JavaScript 類別(class)中,用於定義只能在類別本身呼叫的 方法 或 屬性。
- 靜態方法不會掛在 prototype 上,因而不會被實例繼承,但 子類別仍會繼承 父類別的 static 成員,並可使用
super呼叫。 - 常見的應用包括 工具函式、工廠方法、全域設定、日誌系統 等,使用 static 可以減少不必要的實例化與記憶體開銷。
- 需要注意的陷阱有 誤用 this、過度集中功能、屬性被意外改寫,遵循 單一職責、純函式、文件化 的原則,可寫出可維護且易測試的程式碼。
掌握靜態方法的正確使用方式,將讓你的 JavaScript 程式設計更具彈性與可讀性,亦能在大型專案中保持代碼結構的清晰與一致。祝你在原型與繼承的世界裡,寫出更優雅的程式!