JavaScript 原型與繼承:super 與繼承機制
簡介
在 JavaScript 中,原型(prototype) 是所有物件共享行為的根本機制,而 繼承 則讓我們可以在不複製程式碼的前提下,建立功能更完整的類別(class)。ES6 引入的 class 語法只是語法糖,背後仍舊是原型鏈的運作。
其中最常讓新手感到疑惑的,就是 super 關鍵字。它不僅能在子類別的建構子(constructor)裡呼叫父類別的建構子,還能在方法中存取父類別原型上的實作。正確掌握 super 的使用方式,能讓我們寫出 可讀性高、維護性好的繼承程式碼,在大型專案或框架開發時尤其重要。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直到實務應用,完整闡述 super 與 JavaScript 繼承機制,幫助讀者從「會用」走向「懂原理」的階段。
核心概念
1. class、extends 與原型鏈
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class定義的是 構造函式(constructor)以及掛在其prototype上的方法。extends會在子類別的原型上建立 [[Prototype]](即[[Prototype]])指向父類別的prototype,形成 原型鏈。
class Dog extends Animal {
// 這裡會自動建立 Dog.prototype.__proto__ === Animal.prototype
}
重點:即使使用
class,底層仍是 原型繼承,instanceof、Object.getPrototypeOf等檢查方式不會因語法糖而改變。
2. super 在建構子(constructor)中的角色
在子類別的建構子裡,必須先呼叫 super(),才能使用 this。super() 會執行父類別的建構子,並把 this 綁定到子類別的實例上。
class Cat extends Animal {
constructor(name, color) {
// 必須先呼叫 super,才能使用 this
super(name); // 呼叫 Animal 的 constructor
this.color = color; // 接著才能設定子類別自己的屬性
}
}
- 若未呼叫
super(),執行時會拋出ReferenceError: Must call super constructor before using 'this' in derived class constructor。
3. super 在方法中的使用
super 也可以在 普通方法(包括 getter、setter)裡呼叫父類別原型上的同名方法。此時 super 代表 父類別的 prototype,而非父類別的建構子。
class Bird extends Animal {
speak() {
// 先執行父類別的 speak,再加上自己的行為
super.speak(); // 呼叫 Animal.prototype.speak
console.log(`${this.name} chirps.`);
}
}
super只能在 類別方法內 使用,若在普通函式(function)或箭頭函式外部使用,會得到ReferenceError。
4. super 與靜態方法(static)
靜態方法屬於類別本身,而非實例。使用 super 於靜態方法時,指向的是 父類別本身(即父類別的 constructor),而非父類別的 prototype。
class Vehicle {
static describe() {
console.log('All vehicles have wheels.');
}
}
class Car extends Vehicle {
static describe() {
super.describe(); // 呼叫 Vehicle.describe
console.log('Cars can transport people.');
}
}
5. super 與 Object.setPrototypeOf、Object.create 的關係
在 ES5 以前,我們常透過 Object.create 或手動設定 prototype 來實作繼承。super 的底層實作,其實就是 把子類別的 prototype 指向父類別的 prototype,而在建構子裡呼叫 super(),則相當於 執行父類別的建構子,並把返回的 this 交給子類別。
// ES5 手寫繼承(等同於 class extends)
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hi, I'm ${this.name}`);
};
function Student(name, school) {
Person.call(this, name); // super(name)
this.school = school;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function () {
console.log(`${this.name} studies at ${this.school}`);
};
程式碼範例
範例 1:基本的 super 呼叫(建構子)
class Shape {
constructor(color) {
this.color = color;
}
describe() {
console.log(`A ${this.color} shape.`);
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color); // 呼叫 Shape 的 constructor
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
describe() {
super.describe(); // 先執行父類別的 describe
console.log(`It's a circle with radius ${this.radius}.`);
}
}
const c = new Circle('red', 5);
c.describe();
// A red shape.
// It's a circle with radius 5.
說明:
super(color)把color傳給父類別,子類別仍然可以自行加入radius。在describe方法裡,super.describe()讓我們不必重複父類別的敘述。
範例 2:super 在 getter / setter 中的應用
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side); // 正方形的寬高相同
}
// 重新定義 getter,仍保留父類別的計算方式
get area() {
console.log('Calculating area of a square...');
return super.area; // 呼叫 Rectangle.prototype.area
}
}
const s = new Square(4);
console.log(s.area);
// Calculating area of a square...
// 16
說明:即使在 getter 裡,
super.area仍然可以存取父類別的計算邏輯,避免重複實作。
範例 3:靜態方法中的 super
class Logger {
static log(message) {
console.log(`[LOG] ${message}`);
}
}
class FileLogger extends Logger {
static log(message) {
super.log(message); // 呼叫 Logger.log
// 實際上會寫入檔案,這裡用 console 代替
console.log(`[FILE] ${message}`);
}
}
FileLogger.log('系統啟動');
// [LOG] 系統啟動
// [FILE] 系統啟動
說明:靜態方法的
super指向父類別本身(Logger),因此可以在子類別的靜態方法中「先執行父類別的行為,再加入自己的額外功能」。
範例 4:super 與 new.target 結合(進階)
class Base {
constructor() {
console.log('Base constructor, new.target:', new.target.name);
}
}
class Derived extends Base {
constructor() {
super(); // Base 的 constructor 會看到 new.target 為 Derived
console.log('Derived constructor');
}
}
new Derived();
// Base constructor, new.target: Derived
// Derived constructor
說明:
new.target會在父類別的建構子裡指向實際被new的子類別,這對於 抽象類別 或 工廠模式 的實作非常有用。
範例 5:多層繼承與 super 的鏈式呼叫
class A {
method() {
console.log('A');
}
}
class B extends A {
method() {
console.log('B');
super.method(); // 呼叫 A.method
}
}
class C extends B {
method() {
console.log('C');
super.method(); // 呼叫 B.method,B 再呼叫 A.method
}
}
new C().method();
// C
// B
// A
說明:
super會沿著 原型鏈 向上搜尋同名方法,形成 鏈式呼叫,這在 Mixin 或 行為疊加 時相當便利。
常見陷阱與最佳實踐
| 陷阱 | 可能的錯誤 | 解決方式 / 最佳實踐 |
|---|---|---|
未先呼叫 super() |
ReferenceError: Must call super constructor before using 'this' |
在子類別建構子最前面 必須 呼叫 super(),即使父類別沒有參數也要寫 super(); |
在箭頭函式中使用 super |
ReferenceError: super 或行為不如預期 |
super 只能在 普通方法 中使用,若需要在回呼裡使用,請改用 function 或先把 super 方法存到變數(const parentMethod = super.method;) |
靜態方法與實例方法混用 super |
呼叫不到預期的父類別方法 | 靜態方法的 super 指向父類別本身,實例方法的 super 指向父類別 prototype。保持概念分離,必要時使用 ClassName.method.call(this, ...) |
多重繼承(Mixin)中 super 的衝突 |
方法被意外覆寫或呼叫錯誤的父方法 | 使用 Mixin 函式(例如 Object.assign)時,避免在同一層級使用 super;若必須,採用 Composition over Inheritance 的設計 |
忘記設定子類別的 constructor |
子類別會自動使用父類別的建構子,導致 this 屬性缺失 |
若子類別需要自己的屬性,一定要寫建構子並呼叫 super();若不需要,保留預設建構子即可 |
最佳實踐
- 只在需要時使用
super:若子類別完全覆寫父類別的方法,且不需要父類別的行為,則不必呼叫super,減少不必要的執行成本。 - 保持建構子簡潔:建構子只負責 初始化屬性,把業務邏輯搬到其他方法,讓
super的呼叫保持單一職責。 - 使用
super取代手動原型操作:避免直接寫Child.prototype = Object.create(Parent.prototype),除非要支援非常舊的環境。 - 在靜態方法中使用
super前先確認父類別是否有相同的靜態成員,避免意外的undefined錯誤。 - 測試多層繼承:使用單元測試驗證每一層
super呼叫的行為,確保未因重構而破壞鏈式呼叫。
實際應用場景
UI 元件庫
- 基底元件(例如
Component)提供生命週期方法mount、unmount。子元件在覆寫mount時,仍需要呼叫super.mount()以完成基礎的 DOM 初始化。
- 基底元件(例如
錯誤處理與自訂例外
- 自訂錯誤類別繼承自
Error,在建構子裡使用super(message),同時加入自訂屬性(如code、status),讓錯誤資訊更完整。
- 自訂錯誤類別繼承自
資料模型與 ORM
- 基礎模型提供通用的 CRUD 方法。子模型(如
User、Post)只需覆寫或擴充特定查詢,並使用super.save()以保留共通的儲存流程。
- 基礎模型提供通用的 CRUD 方法。子模型(如
服務端框架(Express、Koa)
- 中間件基底類別實作
handle(req, res, next),子類別可在呼叫super.handle前後加入前置或後置處理,形成 可組合的管線。
- 中間件基底類別實作
遊戲開發
- 角色(
Character)基底類別提供move、attack等方法。不同類型的角色(Warrior、Mage)在覆寫attack時,用super.attack()先執行共通的傷害計算,再加入自己的特殊效果。
- 角色(
總結
super是 ES6 class 中連結子類別與父類別的關鍵橋樑,分別在 建構子、實例方法、getter/setter 以及 靜態方法 中扮演不同角色。- 它的底層機制仍是 原型鏈,只是在語法層面提供了更直覺、可讀性高的呼叫方式。
- 正確使用
super能避免 重複程式碼、提升 維護性,同時在多層繼承時保證 行為的正確疊加。 - 常見的錯誤多與 忘記呼叫
super()、在錯誤的上下文使用super有關,遵守「先呼叫super再使用this」的原則即可避免大部分問題。 - 在實務開發中,從 UI 元件、錯誤處理、資料模型到遊戲角色等各種場景,都能看到
super與繼承的身影。掌握它不僅是語法層面的需求,更是 設計可擴充、可維護系統 的基礎。
實踐建議:在新專案中,先以
class+extends+super為主,除非真的需要兼容非常舊的瀏覽器,才回退到手動操作 prototype。持續寫單元測試,確保每一次繼承的變更都不會破壞既有行為。
祝你在 JavaScript 的原型與繼承世界裡寫出更乾淨、更強大的程式碼! 🚀