JavaScript 課程 ── 原型與繼承
單元:Prototype & Inheritance
主題:class 與 extends
簡介
在 JavaScript 的演化過程中,原型(prototype)一直是語言的核心機制,而 ES6(ECMAScript 2015)則引入了 class 語法,讓開發者可以用更接近傳統物件導向語言(如 Java、C#)的方式描述類別與繼承。
雖然 class 只是一層語法糖,底層仍然是基於原型鏈運作,但它大幅提升了程式碼的可讀性、可維護性,也減少了因手動設定 prototype 而產生的錯誤。
本篇文章將深入說明 class 與 extends 的使用方式、背後原理、常見陷阱以及實務上的最佳實踐,幫助你從「了解」走向「能熟練運用」。
核心概念
1. class 的基本結構
class Person {
// 建構子:建立新實例時會被呼叫
constructor(name, age) {
this.name = name;
this.age = age;
}
// 方法會被放到 prototype 上
greet() {
console.log(`哈囉,我叫 ${this.name},${this.age} 歲。`);
}
// 靜態方法不屬於實例,而是屬於類別本身
static species() {
return 'Homo sapiens';
}
}
constructor:唯一可以使用super()的地方,用來初始化實例屬性。- 方法(如
greet)會自動掛在Person.prototype,因此所有Person實例共享同一份函式。 static方法則掛在Person本身,呼叫方式為Person.species()。
重點:在
class內部宣告的屬性(例如this.name)仍然是 實例屬性,而不是類別屬性。
2. extends:實作繼承
class Employee extends Person {
constructor(name, age, title) {
// 必須先呼叫 super(),才能使用 this
super(name, age);
this.title = title;
}
// 覆寫(override)父類別的方法
greet() {
// 呼叫父類別的 greet,保留原本行為
super.greet();
console.log(`我的職稱是 ${this.title}。`);
}
// 子類別獨有的靜態方法
static role() {
return '員工';
}
}
extends會建立 原型鏈:Employee.prototype.__proto__ === Person.prototype,同時Employee.__proto__ === Person(讓靜態屬性也能繼承)。super有兩種用法:- 在建構子中呼叫
super(...)→ 呼叫父類別的constructor。 - 在方法中呼叫
super.methodName(...)→ 呼叫父類別的同名方法。
- 在建構子中呼叫
3. 私有屬性與存取子(Private fields & getters/setters)
自 ES2022 起,JavaScript 支援 私有欄位(以 # 開頭)以及 存取子,讓類別的封裝性更完整。
class BankAccount {
// 私有欄位
#balance = 0;
constructor(owner) {
this.owner = owner;
}
// 公開方法:存款
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
// 公開方法:提款
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) this.#balance -= amount;
}
// 只讀的 getter
get balance() {
return this.#balance;
}
}
- 私有欄位只能在同一個類別內部直接存取,外部即使
account.#balance也會拋出SyntaxError。 get/set讓屬性看起來像普通屬性,但背後可以加入驗證或計算邏輯。
4. 多層繼承與抽象類別的模擬
雖然 JavaScript 沒有原生的 抽象類別,但可以透過拋出錯誤的方式模擬。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('Shape 為抽象類別,不能直接實例化');
}
}
area() {
throw new Error('子類別必須實作 area 方法');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
new.target只在建構子被直接呼叫時有值,利用它可以防止抽象類別被實例化。- 子類別必須實作
area,否則會在執行時拋出錯誤,確保介面一致性。
5. class 與傳統原型寫法的比較
| 觀點 | class(ES6) |
傳統原型寫法 |
|---|---|---|
| 語法 | class A {} |
function A(){} + A.prototype.method = function(){} |
| 可讀性 | 類似 OOP 語言,結構清晰 | 需要自行維護 prototype,易出錯 |
| 繼承 | extends + super |
Object.create 或手動設定 prototype |
| 靜態成員 | static 關鍵字 |
手動掛在建構子上 A.staticMethod = ... |
| 私有欄位 | #field(ES2022) |
只能透過閉包或 Symbol 隱蔽 |
結論:在新專案或需要長期維護的程式碼中,優先使用
class語法;舊有程式碼若已大量使用原型,則不必強行改寫,除非有明顯的可讀性或功能需求。
常見陷阱與最佳實踐
1. 忘記呼叫 super()
class Child extends Parent {
constructor() {
// ❌ 錯誤:未呼叫 super()
this.prop = 1; // 會拋出 ReferenceError
}
}
解決方法:在子類別建構子第一行必須先 super(...args),才能使用 this。
2. super 只能在方法內使用
super 只能出現在 建構子 或 普通方法(不包括箭頭函式)裡。
class Foo {
method = () => {
super.bar(); // ❌ SyntaxError:super 只能在一般函式內使用
}
}
最佳實踐:若需要在箭頭函式裡呼叫父方法,先把父方法存成變數再使用。
class Foo extends Base {
method = () => {
const parentBar = super.bar;
parentBar.call(this);
}
}
3. 靜態屬性與實例屬性的混淆
class Demo {
static count = 0; // ES2022 靜態欄位
value = 1; // 實例屬性
}
Demo.count為類別本身的屬性,所有實例共享。new Demo().value為每個實例獨有。
建議:使用 static 來儲存「類別層級」的資訊(例如計數器、工廠方法),避免把本應是實例屬性的資料寫成靜態的。
4. 多重繼承的限制
JavaScript 只支援 單一繼承(一個類別只能 extends 一個父類別),若需要混入(mixin)功能,可使用 函式混入或 Object.assign:
const Flyer = Base => class extends Base {
fly() { console.log(`${this.name} 正在飛行`); }
};
class Bird extends Flyer(Animal) {
constructor(name) {
super(name);
}
}
5. 盡量避免在建構子裡做大量運算
建構子應該只負責 初始化屬性,繁重的計算或 I/O(如 fetch)應該放在獨立的初始化方法或 async 工廠函式中,避免 new Class() 時阻塞。
class DataLoader {
constructor(url) {
this.url = url;
// ❌ 不建議在此直接 fetch
}
async load() {
const resp = await fetch(this.url);
this.data = await resp.json();
}
}
實際應用場景
1. 前端 UI 元件基礎類別
class UIComponent {
constructor(root) {
this.root = root; // DOM 元素
}
show() { this.root.style.display = ''; }
hide() { this.root.style.display = 'none'; }
}
class Modal extends UIComponent {
constructor(root, title) {
super(root);
this.title = title;
}
open() {
this.show();
console.log(`開啟 Modal:${this.title}`);
}
close() {
this.hide();
console.log('關閉 Modal');
}
}
- 透過
class建立共通的 UI 行為(show/hide),子類別只關注自身特有的邏輯。
2. Node.js 服務端的資料模型
// models/User.js
class User {
constructor({ id, name, email }) {
this.id = id;
this.name = name;
this.email = email;
}
// 範例:驗證 email 格式
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// 實例方法:更新名稱
rename(newName) {
this.name = newName;
}
}
// models/Admin.js
class Admin extends User {
constructor(props) {
super(props);
this.role = 'admin';
}
// 覆寫
rename(newName) {
if (!User.isValidEmail(newName)) {
throw new Error('管理員名稱必須是有效的 email');
}
super.rename(newName);
}
}
- 透過繼承,
Admin直接取得User的屬性與方法,同時加入權限相關的驗證。
3. 遊戲開發:角色系統
class Character {
constructor(name, hp) {
this.name = name;
this.hp = hp;
}
attack(target) {
const dmg = Math.floor(Math.random() * 10) + 1;
console.log(`${this.name} 攻擊 ${target.name},造成 ${dmg} 點傷害`);
target.takeDamage(dmg);
}
takeDamage(amount) {
this.hp -= amount;
if (this.hp <= 0) console.log(`${this.name} 已倒下`);
}
}
class Warrior extends Character {
constructor(name) {
super(name, 120);
this.rage = 0;
}
attack(target) {
super.attack(target);
this.rage += 10;
console.log(`${this.name} 的怒氣提升至 ${this.rage}`);
}
}
Warrior繼承Character,在attack後額外處理「怒氣」機制,展示 方法覆寫 與 額外屬性 的結合。
總結
class與extends為 ES6 引入的語法糖,讓 原型繼承 更直觀、易讀。constructor、super、static、私有欄位(#field)與存取子(get/set)是最常使用的特性。- 使用
class時要特別留意:- 子類別必須在建構子第一行呼叫
super()。 super僅能在普通方法或建構子中使用。- 靜態與實例屬性的差異,避免混淆。
- 子類別必須在建構子第一行呼叫
- 在實務開發中,
class常被運用於 UI 元件、資料模型、遊戲角色 等需要共用行為與屬性的情境。 - 雖然底層仍是原型鏈,但透過
class我們可以更清晰地表達「什麼是」與「如何繼承」的概念,提升團隊合作與程式碼可維護性。
**掌握
class與extends,就等於掌握了 JavaScript 物件導向的核心。**未來在開發大型應用或框架時,你會發現這套語法不僅讓程式碼更具結構,也讓除錯與測試變得更加簡潔。祝你寫程式愉快,持續在 JavaScript 的世界裡探索與成長!