ES6+ 新特性:class 與繼承
簡介
在 JavaScript 的歷史演進中,class 於 ES6(ECMAScript 2015)正式登場,為物件導向程式設計(OOP)提供了更直觀、類似其他語言(如 Java、C#)的語法糖。雖然底層仍然是原型鏈(prototype)運作,使用 class 可以讓開發者以「類別」的概念去描述 資料 與 行為,提升程式碼可讀性與維護性。
隨著前端框架(React、Vue、Angular)以及 Node.js 生態系的持續成長,繼承(inheritance)已成為構建可重用、可擴充模組的關鍵技術。掌握 ES6+ 的 class 與繼承,不僅能寫出更乾淨的程式碼,也能在團隊協作時減少誤解與錯誤。
本篇文章將從 class 的基本語法、建構子與屬性、方法與存取子(getter / setter)、繼承與 super,以及 靜態屬性/方法 等核心概念切入,配合實用範例說明,最後彙整常見陷阱、最佳實踐與實務應用情境,協助你在日常開發中快速上手並避免踩雷。
核心概念
1. class 基礎語法
class 的宣告方式類似傳統語言的 class,但在 JavaScript 中它僅是 語法糖,最終仍會被編譯成原型鏈的操作。基本語法如下:
// 宣告一個簡單的類別
class Person {
// 建構子 (constructor) 會在建立實例時自動執行
constructor(name) {
this.name = name; // 為每個實例設定 name 屬性
}
// 方法 (method) 會被放到 prototype 上
greet() {
console.log(`Hello, I'm ${this.name}.`);
}
}
// 使用 class 建立實例
const alice = new Person('Alice');
alice.greet(); // => Hello, I'm Alice.
class必須使用new關鍵字 來產生實例,直接呼叫Person()會拋出TypeError。- 所有方法都屬於 prototype,因此不會在每次建立實例時重新建立函式,節省記憶體。
2. 建構子與屬性
constructor 是類別的特殊方法,負責初始化實例屬性。若未自行定義,JavaScript 會自動產生一個空的建構子。
class Rectangle {
// 建構子接受寬與高,並計算面積
constructor(width, height) {
this.width = width;
this.height = height;
}
// 計算面積的實例方法
area() {
return this.width * this.height;
}
}
// 建立一個 10x5 的矩形
const rect = new Rectangle(10, 5);
console.log(rect.area()); // => 50
小技巧:如果你只想在建構子裡做簡單的屬性賦值,可以直接使用參數屬性(parameter properties)結合 解構賦值,寫得更簡潔:
class Point {
constructor({ x = 0, y = 0 } = {}) {
Object.assign(this, { x, y });
}
}
const p = new Point({ x: 3, y: 4 });
console.log(p); // => Point { x: 3, y: 4 }
3. 方法、getter / setter
除了普通方法外,ES6 允許在 class 中定義 getter 與 setter,讓屬性讀寫時能自動觸發額外邏輯。
class Circle {
constructor(radius) {
this._radius = radius; // 使用底線作為私有屬性慣例
}
// getter:取得直徑
get diameter() {
return this._radius * 2;
}
// setter:設定直徑時自動更新半徑
set diameter(d) {
this._radius = d / 2;
}
// 一般方法:計算面積
area() {
return Math.PI * this._radius ** 2;
}
}
const c = new Circle(5);
console.log(c.diameter); // => 10
c.diameter = 20; // 透過 setter 更新半徑
console.log(c.area()); // => 約 314.16
- getter / setter 讓外部看起來像在直接操作屬性,實際上卻可以加入驗證、快取或副作用。
- 注意:在
strict mode(ES6 模組預設即為 strict)下,若在 getter 中遺漏return,會回傳undefined,容易產生錯誤。
4. 繼承(extends、super)
extends 用於建立子類別,子類別會自動繼承父類別的 prototype 方法與屬性。super 則分為兩種情境:
- 在建構子內呼叫
super(...),必須在使用this前先呼叫父類別的建構子。 - 在方法內呼叫
super.methodName(),可存取父類別的同名方法(常用於「覆寫」)。
// 父類別:Vehicle
class Vehicle {
constructor(make) {
this.make = make;
}
start() {
console.log(`${this.make} engine started.`);
}
}
// 子類別:Car 繼承 Vehicle
class Car extends Vehicle {
constructor(make, model) {
// 必須先呼叫 super 才能使用 this
super(make);
this.model = model;
}
// 覆寫父類別的 start 方法,但仍想保留原有行為
start() {
super.start(); // 呼叫父類別的 start
console.log(`${this.make} ${this.model} is ready to go!`);
}
}
const myCar = new Car('Toyota', 'Corolla');
myCar.start();
// => Toyota engine started.
// => Toyota Corolla is ready to go!
多層繼承示例
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Mammal extends Animal {
constructor(name, hasFur = true) {
super(name);
this.hasFur = hasFur;
}
}
class Dog extends Mammal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
// 只保留 Dog 的叫聲,不呼叫 super.speak()
console.log(`${this.name} barks!`);
}
}
const rover = new Dog('Rover', 'Golden Retriever');
rover.speak(); // => Rover barks!
重點:子類別若沒有自行定義建構子,會自動使用父類別的建構子;但一旦自行定義,就 必須 在最前面呼叫
super()(除非該類別不繼承任何類別)。
5. 靜態屬性與方法
static 關鍵字可將屬性或方法直接掛在類別本身,而非實例上。常用於工具函式、常量或「工廠方法」。
class MathUtil {
// 靜態常量
static PI = 3.1415926535;
// 靜態方法
static circleArea(radius) {
return MathUtil.PI * radius ** 2;
}
}
// 直接透過類別呼叫
console.log(MathUtil.PI); // => 3.1415926535
console.log(MathUtil.circleArea(3)); // => 約 28.27
// 靜態方法不會出現在實例上
const util = new MathUtil();
console.log(util.circleArea); // => undefined
- 靜態成員 不能被
extends的子類別繼承成實例屬性,但子類別仍可 直接 透過super取得(例如class Sub extends MathUtil {}中Sub.PI仍可存取)。 - 常見用途:API 客戶端的
request方法、資料模型的findById、或是框架的Component基底類別的render靜態方法。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
忘記呼叫 super() |
在子類別建構子中未先呼叫 super(),會在執行 this 前拋出 ReferenceError。 |
永遠在使用 this 前 呼叫 super(...args),即使父類別建構子沒有參數,也要寫 super()。 |
| 在 getter 裡寫副作用 | Getter 被頻繁呼叫,若裡面執行大量計算或 I/O,會影響效能。 | 只在 getter 中返回值;如需額外計算,使用 memoization 或將邏輯搬到普通方法。 |
| 靜態方法與實例屬性混用 | 把應該是實例方法寫成 static,導致無法存取 this,或相反。 |
明確區分「屬於類別」與「屬於實例」的責任,遵循 Single Responsibility Principle。 |
| 過度繼承 | 多層繼承使類別關係複雜,難以追蹤屬性來源。 | 優先考慮 組合(composition),如使用 mixin 或委派(delegation)。 |
未使用 #private |
前 ES2022 只能靠命名慣例(底線)模擬私有屬性,仍可被外部存取。 | 若環境支援,使用 私有欄位 #field,提升封裝性與安全性。 |
// 私有欄位範例(ES2022+)
class Counter {
#count = 0; // 真正的私有屬性
increment() {
this.#count++;
}
get value() {
return this.#count;
}
}
const c = new Counter();
c.increment();
console.log(c.value); // => 1
console.log(c.#count); // SyntaxError
實際應用場景
前端 UI 元件基底類別
- 使用
class Component定義render()、setState()等通用方法,子元件繼承後只需實作自己的 UI。
- 使用
Node.js 服務的資料模型
- 以
class User extends Model方式封裝資料庫 CRUD,讓每個模型只關心自己的 schema 與業務邏輯。
- 以
遊戲開發中的實體系統
class Entity為所有遊戲物件的基礎,class Player extends Entity、class Enemy extends Entity各自實作特有行為,並利用super.update()共享共通更新邏輯。
API 客戶端的工廠模式
class ApiClient包含static createInstance(config),返回已設定好 baseURL、攔截器的實例,讓其他模組只需要呼叫ApiClient.get('/users')。
程式碼生成器或 DSL
- 利用
class Builder內的鏈式方法(return this)建立複雜物件,最終透過build()產出結果,類似 React 的 JSX 轉換流程。
- 利用
總結
ES6+ 的 class 為 JavaScript 帶來了更清晰的物件導向語法,讓開發者可以:
- 以類別的概念 抽象出共通行為與屬性。
- 透過
extends/super建立層次分明的繼承結構,重用與擴充程式碼。 - 使用 getter / setter、static,提升封裝性與工具函式的可達性。
- 結合私有欄位
#、mixins,在保持可讀性的同時避免過度繼承所帶來的複雜度。
在實務開發中,適度運用 class 與繼承可以讓 程式碼更具可維護性、可測試性,同時也符合現代前端/後端框架的設計哲學。當然,不要盲目追求繼承,根據需求選擇組合或 mixin 才是長期維護的關鍵。
掌握以上概念與最佳實踐後,你就能在專案中自如地使用 class,寫出結構良好、易於擴充的 JavaScript 程式碼。祝你開發順利,持續探索更進階的 ESNext 功能!