本文 AI 產出,尚未審核

JavaScript 課程 ── 原型與繼承

單元:Prototype & Inheritance

主題:classextends


簡介

在 JavaScript 的演化過程中,原型(prototype)一直是語言的核心機制,而 ES6(ECMAScript 2015)則引入了 class 語法,讓開發者可以用更接近傳統物件導向語言(如 Java、C#)的方式描述類別繼承
雖然 class 只是一層語法糖,底層仍然是基於原型鏈運作,但它大幅提升了程式碼的可讀性、可維護性,也減少了因手動設定 prototype 而產生的錯誤。
本篇文章將深入說明 classextends 的使用方式、背後原理、常見陷阱以及實務上的最佳實踐,幫助你從「了解」走向「能熟練運用」。


核心概念

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 後額外處理「怒氣」機制,展示 方法覆寫額外屬性 的結合。

總結

  • classextends 為 ES6 引入的語法糖,讓 原型繼承 更直觀、易讀。
  • constructorsuperstatic、私有欄位(#field)與存取子(get/set)是最常使用的特性。
  • 使用 class 時要特別留意:
    1. 子類別必須在建構子第一行呼叫 super()
    2. super 僅能在普通方法或建構子中使用。
    3. 靜態與實例屬性的差異,避免混淆。
  • 在實務開發中,class 常被運用於 UI 元件、資料模型、遊戲角色 等需要共用行為與屬性的情境。
  • 雖然底層仍是原型鏈,但透過 class 我們可以更清晰地表達「什麼是」與「如何繼承」的概念,提升團隊合作與程式碼可維護性。

**掌握 classextends,就等於掌握了 JavaScript 物件導向的核心。**未來在開發大型應用或框架時,你會發現這套語法不僅讓程式碼更具結構,也讓除錯與測試變得更加簡潔。祝你寫程式愉快,持續在 JavaScript 的世界裡探索與成長!