JavaScript 原型與繼承:原型鍊(Prototype Chain)
簡介
在 JavaScript 中,物件是唯一的資料結構,而所有物件的行為與屬性都透過「原型」來共享與繼承。
原型鍊(prototype chain)是 JavaScript 物件導向的核心機制,它決定了屬性與方法的查找順序,也讓我們能在不使用傳統類別繼承的情況下,實現程式碼的重用與擴充。
對於初學者而言,了解原型鍊能幫助破除「物件找不到屬性」的疑惑;對中階開發者來說,則是優化效能、設計可維護 API 的重要基礎。本文將以淺顯易懂的方式,從概念說明到實務範例,完整剖析原型鍊的運作原理與最佳實踐。
核心概念
1. 什麼是「原型」?
每個 JavaScript 物件在建立時,都會自動帶有一個隱藏的屬性 [[Prototype]](在程式碼中可透過 __proto__ 取得),指向另一個物件。這個被指向的物件稱為 原型,它本身也可能有自己的原型,形成一條向上延伸的鏈條——原型鍊。
重點:只有 物件 才有原型;原始值(如
number、string)在使用屬性或方法時,會暫時被包裝成對應的物件類型(Number、String),再參與原型鍊的查找。
2. 原型鍊的運作流程
當我們對物件 obj 讀取屬性 prop 時,JavaScript 會依序進行以下步驟:
- 自有屬性檢查:先在
obj本身(Own Property)尋找prop。 - 原型檢查:若找不到,則沿著
obj.__proto__(即obj的原型)繼續尋找。 - 遞迴上溯:重複步驟 2,直到到達最頂層的
Object.prototype。 - 結束:若仍未找到,返回
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
counterA與counterB各自擁有自己的 自有屬性value(在inc時被寫入),但共享同一套方法inc、dec,因為這些方法位於共同的原型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 取代深層繼承。 |
最佳實踐小結
- 只在原型上放方法,屬性(特別是可變的)放在建構子。
- 使用
class讓繼承結構更清晰,同時保留原型鍊的彈性。 - 避免直接改動全域原型,以免產生安全漏洞(Prototype Pollution)。
- 利用
Object.create建立純粹的原型鏈,適合輕量的資料結構。 - 測試原型鍊:使用
Object.getPrototypeOf(obj) === ExpectedProto確認繼承關係正確。
實際應用場景
- UI 元件庫:透過原型鍊共享共通的渲染與事件處理方法,子元件只需實作差異化的行為。
- 資料模型 (ORM):基礎模型提供 CRUD 方法,特定資料表的模型只需要擴充屬性與自訂查詢。
- 插件系統:主程式提供核心 API(放在原型上),外掛透過
Object.setPrototypeOf把自己掛在主體上,形成可擴充的鏈條。 - 測試 Mock:在單元測試時,可臨時把物件的原型指向測試用的 stub,避免改動實際程式碼。
- 多語系字串管理:共用的
translate方法放在原型上,不同語系的物件只需提供對應的字典資料。
總結
- 原型鍊 是 JavaScript 物件導向的基礎機制,決定了屬性與方法的查找順序。
- 透過
prototype、__proto__(或正式的Object.getPrototypeOf)以及Object.create,我們可以靈活地建立、調整與查驗原型關係。 - 正確使用原型 能讓程式碼更具可重用性與維護性;相反,濫用或污染原型 則會帶來難以排除的錯誤與安全風險。
- 在實務開發中,原型鍊常被用於 UI 元件繼承、資料模型抽象、插件機制 等情境,配合 class 語法 或 Mixin,即可寫出既清晰又高效的程式碼。
掌握原型鍊的運作,等於掌握了 JavaScript 的「血脈」——未來無論是撰寫簡單腳本,還是構建大型應用,都能以更自信的姿態面對物件導向的挑戰。祝你在程式之路上,原型不斷、繼承無礙!