本文 AI 產出,尚未審核

JavaScript 原型與繼承:原型鍊(Prototype Chain)


簡介

在 JavaScript 中,物件是唯一的資料結構,而所有物件的行為與屬性都透過「原型」來共享與繼承。
原型鍊(prototype chain)是 JavaScript 物件導向的核心機制,它決定了屬性與方法的查找順序,也讓我們能在不使用傳統類別繼承的情況下,實現程式碼的重用與擴充。

對於初學者而言,了解原型鍊能幫助破除「物件找不到屬性」的疑惑;對中階開發者來說,則是優化效能、設計可維護 API 的重要基礎。本文將以淺顯易懂的方式,從概念說明到實務範例,完整剖析原型鍊的運作原理與最佳實踐。


核心概念

1. 什麼是「原型」?

每個 JavaScript 物件在建立時,都會自動帶有一個隱藏的屬性 [[Prototype]](在程式碼中可透過 __proto__ 取得),指向另一個物件。這個被指向的物件稱為 原型,它本身也可能有自己的原型,形成一條向上延伸的鏈條——原型鍊

重點:只有 物件 才有原型;原始值(如 numberstring)在使用屬性或方法時,會暫時被包裝成對應的物件類型(NumberString),再參與原型鍊的查找。

2. 原型鍊的運作流程

當我們對物件 obj 讀取屬性 prop 時,JavaScript 會依序進行以下步驟:

  1. 自有屬性檢查:先在 obj 本身(Own Property)尋找 prop
  2. 原型檢查:若找不到,則沿著 obj.__proto__(即 obj 的原型)繼續尋找。
  3. 遞迴上溯:重複步驟 2,直到到達最頂層的 Object.prototype
  4. 結束:若仍未找到,返回 undefined

這條「向上」的搜尋路徑,就是 原型鍊

3. prototype__proto__ 的差異

屬性 所屬對象 目的 常見用法
prototype 函式 (constructor) 定義 所有由該建構子建立的實例 共享的屬性與方法 function Person(){}
Person.prototype.greet = function(){}
__proto__ 物件(實例) 指向該物件的 原型,實際參與屬性查找 obj.__proto__ === Person.prototype

Tip:在 ES6 之後,官方建議使用 Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto) 來取代直接存取 __proto__,以提高相容性與可讀性。

4. 屬性查找實例

function Animal(name) {
  this.name = name;               // ← 自有屬性
}
Animal.prototype.sayHello = function () {
  console.log(`Hello, I am ${this.name}`);
};

function Dog(name, breed) {
  Animal.call(this, name);        // 繼承 name 屬性
  this.breed = breed;             // ← 自有屬性
}
// 設定 Dog 的原型指向 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修正 constructor

Dog.prototype.bark = function () {
  console.log('Woof! Woof!');
};

const mimi = new Dog('Mimi', 'Poodle');
mimi.sayHello(); // 從 Dog.prototype → Animal.prototype 找到
mimi.bark();     // 直接在 Dog.prototype 找到

在上述程式碼中,mimi 會先在自己的屬性(name, breed)尋找 sayHello,找不到後沿著 Dog.prototype(第一層),再往上到 Animal.prototype(第二層),最後到 Object.prototype

5. 使用 Object.create 建立自訂原型鍊

Object.create(proto, propertiesObject) 可以直接以指定的原型建立新物件,省去建構子函式的繁瑣。

// 建立一個簡易的「計數器」原型
const counterProto = {
  value: 0,
  inc() {
    this.value++;
    return this.value;
  },
  dec() {
    this.value--;
    return this.value;
  },
};

const counterA = Object.create(counterProto);
const counterB = Object.create(counterProto);

counterA.inc(); // 1
counterB.inc(); // 1
counterA.inc(); // 2

console.log(counterA.value); // 2
console.log(counterB.value); // 1

counterAcounterB 各自擁有自己的 自有屬性 value(在 inc 時被寫入),但共享同一套方法 incdec,因為這些方法位於共同的原型 counterProto 上。

6. class 語法與原型鍊的關係

ES6 引入的 class 其實只是 語法糖,背後仍然是透過原型鏈實作。

class Vehicle {
  constructor(type) {
    this.type = type; // 自有屬性
  }
  move() {
    console.log(`${this.type} is moving`);
  }
}

class Car extends Vehicle {
  constructor(brand) {
    super('car');      // 呼叫父類別建構子
    this.brand = brand;
  }
  honk() {
    console.log('Beep beep!');
  }
}

const tesla = new Car('Tesla');
tesla.move(); // 從 Car.prototype → Vehicle.prototype 找到
tesla.honk(); // 直接在 Car.prototype 找到

Car.prototype.__proto__ === Vehicle.prototype,這條鏈條正是 class 繼承 所依賴的原型鍊。


程式碼範例(實用範例 3~5 個)

範例 1:模擬多層繼承的「人物」系統

// 基礎 Person
function Person(name) {
  this.name = name;
}
Person.prototype.introduce = function () {
  console.log(`Hi, I'm ${this.name}`);
};

// Employee 繼承 Person
function Employee(name, company) {
  Person.call(this, name); // 繼承 name
  this.company = company;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.work = function () {
  console.log(`${this.name} works at ${this.company}`);
};

// Manager 繼承 Employee
function Manager(name, company, teamSize) {
  Employee.call(this, name, company);
  this.teamSize = teamSize;
}
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.lead = function () {
  console.log(`${this.name} leads a team of ${this.teamSize} people`);
};

const alice = new Manager('Alice', 'Acme Corp', 5);
alice.introduce(); // Hi, I'm Alice          (從 Person.prototype)
alice.work();      // Alice works at Acme Corp (從 Employee.prototype)
alice.lead();      // Alice leads a team of 5 people (從 Manager.prototype)

這個範例展示 三層原型鍊Manager → Employee → Person → Object.prototype,每一層都只負責自己應該提供的功能,保持職責單一。

範例 2:使用 Object.setPrototypeOf 動態改變原型

const animal = {
  eat() { console.log(`${this.name} eats`); }
};

const dog = { name: 'Buddy' };
Object.setPrototypeOf(dog, animal); // 動態設定原型

dog.eat(); // Buddy eats

在某些情況(如測試環境或插件機制)需要 在執行時 改變物件的原型,Object.setPrototypeOf 提供了安全且可讀的方式。

範例 3:避免原型污染(Prototype Pollution)

// 不安全的做法:直接修改 Object.prototype
Object.prototype.isAdmin = false;

// 任何物件都會繼承 isAdmin
const user = { name: 'Tom' };
console.log(user.isAdmin); // false

// 正確做法:使用自訂的原型物件
const safeProto = { isAdmin: false };
const safeUser = Object.create(safeProto);
console.log(safeUser.isAdmin); // false

原型污染 會導致所有物件意外取得或修改共用屬性,安全的做法是 不直接修改 Object.prototype,而是建立自己的原型物件。

範例 4:利用原型鍊實作「混入」(Mixin)

const canFly = {
  fly() { console.log(`${this.name} is flying!`); }
};

function Bird(name) {
  this.name = name;
}
Bird.prototype = Object.create(canFly); // 混入 canFly 的方法
Bird.prototype.constructor = Bird;

const sparrow = new Bird('Sparrow');
sparrow.fly(); // Sparrow is flying!

Mixin 透過 把功能物件作為原型,讓多個類別共享同一套方法,避免重複實作。

範例 5:在 class 中使用 super 觸發原型鏈

class Shape {
  constructor(color) {
    this.color = color;
  }
  getInfo() {
    return `Color: ${this.color}`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);          // 呼叫父類別建構子
    this.radius = radius;
  }
  getInfo() {
    // 先取得父類別的資訊,再加上自己的屬性
    return `${super.getInfo()}, Radius: ${this.radius}`;
  }
}

const c = new Circle('red', 10);
console.log(c.getInfo()); // Color: red, Radius: 10

這裡的 super.getInfo() 實際上是 沿著原型鍊 往上搜尋 Shape.prototype.getInfo,再回傳結果。


常見陷阱與最佳實踐

陷阱 說明 建議的最佳實踐
直接修改 Object.prototype 會造成全域污染,導致不可預期的行為。 永遠避免,改用自訂原型或 Object.create
忘記恢復 constructor 設定子類別原型後,constructor 會指向父類別。 手動設定 Child.prototype.constructor = Child,或使用 class 語法。
使用 __proto__ 取代正式 API __proto__ 在舊版瀏覽器尚未標準化,且效能較差。 使用 Object.getPrototypeOf / Object.setPrototypeOf
在原型上放置可變資料 原型屬性是所有實例共享的,若是陣列或物件會被所有實例共用。 只在原型上放 函式或不可變值,可變資料應放在建構子內。
過度繼承 多層繼承會使原型鍊過長,搜尋成本提升,且維護困難。 盡量使用 組合 (Composition)Mixin 取代深層繼承。

最佳實踐小結

  1. 只在原型上放方法,屬性(特別是可變的)放在建構子。
  2. 使用 class 讓繼承結構更清晰,同時保留原型鍊的彈性。
  3. 避免直接改動全域原型,以免產生安全漏洞(Prototype Pollution)。
  4. 利用 Object.create 建立純粹的原型鏈,適合輕量的資料結構。
  5. 測試原型鍊:使用 Object.getPrototypeOf(obj) === ExpectedProto 確認繼承關係正確。

實際應用場景

  1. UI 元件庫:透過原型鍊共享共通的渲染與事件處理方法,子元件只需實作差異化的行為。
  2. 資料模型 (ORM):基礎模型提供 CRUD 方法,特定資料表的模型只需要擴充屬性與自訂查詢。
  3. 插件系統:主程式提供核心 API(放在原型上),外掛透過 Object.setPrototypeOf 把自己掛在主體上,形成可擴充的鏈條。
  4. 測試 Mock:在單元測試時,可臨時把物件的原型指向測試用的 stub,避免改動實際程式碼。
  5. 多語系字串管理:共用的 translate 方法放在原型上,不同語系的物件只需提供對應的字典資料。

總結

  • 原型鍊 是 JavaScript 物件導向的基礎機制,決定了屬性與方法的查找順序。
  • 透過 prototype__proto__(或正式的 Object.getPrototypeOf)以及 Object.create,我們可以靈活地建立、調整與查驗原型關係。
  • 正確使用原型 能讓程式碼更具可重用性與維護性;相反,濫用或污染原型 則會帶來難以排除的錯誤與安全風險。
  • 在實務開發中,原型鍊常被用於 UI 元件繼承、資料模型抽象、插件機制 等情境,配合 class 語法Mixin,即可寫出既清晰又高效的程式碼。

掌握原型鍊的運作,等於掌握了 JavaScript 的「血脈」——未來無論是撰寫簡單腳本,還是構建大型應用,都能以更自信的姿態面對物件導向的挑戰。祝你在程式之路上,原型不斷、繼承無礙