JavaScript 原型與繼承:Object.getPrototypeOf() 與 Object.setPrototypeOf()
簡介
在 JavaScript 中,原型(prototype) 是物件繼承的核心機制。每個物件都有一條隱藏的連結指向另一個物件──它的原型。透過原型鏈,屬性與方法得以在不同物件之間共享,形成所謂的「原型繼承」。
ES5 之後,Object.getPrototypeOf() 與 Object.setPrototypeOf() 兩個靜態方法提供了官方且安全的方式,讓開發者能夠在程式執行時檢查或變更物件的原型。相較於舊式的 __proto__ 屬性,這兩個方法更符合規範、在所有主流瀏覽器與 Node.js 中都有良好支援。
本篇文章將深入說明這兩個 API 的用法、背後原理、常見陷阱與最佳實踐,並提供實務範例,幫助你在開發中正確運用原型與繼承。
核心概念
1. 什麼是原型?
- 每個 普通物件 (
{}) 皆有內部屬性[[Prototype]],指向另一個物件或null。 - 當存取一個屬性時,JavaScript 會先在自身上找,找不到才沿著原型鏈往上搜尋。
- 原型本身也是物件,因而可以再有自己的原型,形成原型鏈。
小技巧:在 Chrome 開發者工具中,
Object.getPrototypeOf(obj) === obj.__proto__(在支援__proto__的環境下)會回傳true,但正式開發仍建議使用Object.getPrototypeOf。
2. Object.getPrototypeOf(obj)
- 功能:取得
obj的原型(即[[Prototype]])。 - 語法:
const proto = Object.getPrototypeOf(obj); - 回傳值:若
obj為原始值(如number、string),會先將其包裝為對應的物件(Number、String)再返回其原型;若obj為null或undefined,會拋出TypeError。
範例 1:基本取得原型
const person = {
name: 'Alice',
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
const proto = Object.getPrototypeOf(person);
console.log(proto === Object.prototype); // true
說明:person 的原型是 Object.prototype,因此可以使用 hasOwnProperty 等方法。
範例 2:取得建構函式的原型
function Car(make) {
this.make = make;
}
Car.prototype.drive = function () {
console.log(`Driving a ${this.make}`);
};
const myCar = new Car('Toyota');
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // true
說明:new Car() 產生的物件的原型正是 Car.prototype,這也是繼承的根本。
3. Object.setPrototypeOf(obj, prototype)
- 功能:設定
obj的原型為prototype(必須是物件或null)。 - 語法:
Object.setPrototypeOf(obj, prototype); - 回傳值:返回修改後的
obj本身,允許鏈式呼叫。 - 注意:此操作在效能上較為昂貴,因為 JavaScript 引擎需要重新評估物件的內部結構。除非真的需要動態改變原型,否則應避免使用。
範例 3:動態改變原型
const animal = {
eat() {
console.log('Eating...');
}
};
const dog = {
bark() {
console.log('Woof!');
}
};
// 原本 dog 沒有 eat 方法
dog.eat(); // TypeError: dog.eat is not a function
// 設定 dog 的原型為 animal
Object.setPrototypeOf(dog, animal);
dog.eat(); // 正常執行,輸出 "Eating..."
dog.bark(); // 仍然可以呼叫自己的方法
說明:透過 Object.setPrototypeOf,dog 立即取得了 animal 的 eat 方法。
範例 4:使用 null 取消原型(建立「純粹」物件)
const dict = Object.create(null); // 直接以 null 為原型
dict.apple = '蘋果';
dict.banana = '香蕉';
console.log(Object.getPrototypeOf(dict)); // null
console.log(dict.hasOwnProperty); // undefined,避免與 Object.prototype 的衝突
說明:Object.create(null) 與 Object.setPrototypeOf(obj, null) 效果相同,常用於需要「純粹」鍵值對的情境(如字典、Map 的簡易實作)。
範例 5:與 ES6 class 結合
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
// 取得 class 實例的原型
const p = new Person('Bob');
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
// 動態改變為另一個 class 的原型
class Robot {
constructor(id) {
this.id = id;
}
beep() {
console.log(`Beep! ID: ${this.id}`);
}
}
Object.setPrototypeOf(p, Robot.prototype);
p.beep(); // Beep! ID: undefined(因為沒有 id 屬性)
說明:雖然可以改變原型,但屬性仍屬於原本的建構函式,必須自行補足。此例展示了可能產生的錯誤情況,提醒開發者慎用。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 建議的解決方案 |
|---|---|---|
| 頻繁改變原型 | Object.setPrototypeOf 會導致引擎重新優化物件,嚴重影響效能。 |
在設計階段就決定好原型結構;若真的需要動態行為,考慮使用 委派(delegation) 或 mixins。 |
使用 null 原型的物件當作常規物件 |
失去 toString、hasOwnProperty 等常用方法,導致意外錯誤。 |
只在需要「純粹字典」時使用 Object.create(null);一般物件仍保留 Object.prototype。 |
| 將非物件當作第二參數 | Object.setPrototypeOf 只接受物件或 null,否則拋出 TypeError。 |
確認第二參數是 Object、Array、Function 等,或先使用 Object(prototype) 包裝。 |
混用 __proto__ 與 Object.setPrototypeOf |
雖然功能相似,但 __proto__ 並非標準,可能在未來被棄用。 |
完全拋棄 __proto__,改用官方 API。 |
改變原型後忘記更新 instanceof 判斷 |
instanceof 依賴原型鏈,改變後舊的判斷可能失效。 |
改變原型後重新檢查或避免在執行期依賴 instanceof。 |
最佳實踐
盡量在建構階段確定原型
使用class、function或Object.create先行設定好原型,避免之後再改。使用
Object.create(proto, properties)建立帶有自訂屬性的物件const proto = { greet() { console.log('Hi'); } }; const obj = Object.create(proto, { name: { value: 'Tom', writable: true, enumerable: true } });在需要「純粹」鍵值對時,選擇
Object.create(null),而不是手動setPrototypeOf為null。檢查瀏覽器相容性:IE11 不支援
Object.setPrototypeOf,若需相容可改用Object.__proto__(僅限舊環境)或改寫為Object.create。避免在性能敏感的迴圈內呼叫
setPrototypeOf,改為一次性設定或使用其他設計模式(如策略模式)。
實際應用場景
1. 模擬多重繼承(mixins)
JavaScript 原生不支援多重繼承,但可透過 Object.setPrototypeOf 暫時把多個行為混入同一個物件:
const canFly = {
fly() { console.log(`${this.name} is flying!`); }
};
const canSwim = {
swim() { console.log(`${this.name} is swimming!`); }
};
function Animal(name) {
this.name = name;
}
Object.setPrototypeOf(Animal.prototype, canFly); // 先混入 fly
Object.setPrototypeOf(canFly, canSwim); // 再混入 swim
const duck = new Animal('Duck');
duck.fly(); // Duck is flying!
duck.swim(); // Duck is swimming!
2. 建立「安全」的字典物件
在處理 JSON 解析或外部資料時,若直接使用 {} 作為鍵值容器,可能會被原型屬性(如 toString)污染。使用 null 原型的物件可避免:
function safeDict() {
return Object.create(null);
}
const dict = safeDict();
dict['__proto__'] = '被污染';
console.log(dict['__proto__']); // '被污染',不會影響原型鏈
3. 動態切換行為(策略模式)
在遊戲或 UI 中,角色/元件可能需要在執行期間切換不同的行為集合:
const aggressive = {
attack() { console.log('猛烈攻擊!'); }
};
const defensive = {
defend() { console.log('防禦姿態!'); }
};
function Player(name) {
this.name = name;
}
const p = new Player('Hero');
Object.setPrototypeOf(p, aggressive);
p.attack(); // 猛烈攻擊!
// 切換策略
Object.setPrototypeOf(p, defensive);
p.defend(); // 防禦姿態!
4. 測試與 Mocking
在單元測試時,常需要把物件的原型改成測試用的假物件(mock):
const realAPI = {
fetch() { return fetch('https://api.example.com'); }
};
function Service() {}
Object.setPrototypeOf(Service.prototype, realAPI);
const svc = new Service();
svc.fetch = () => Promise.resolve({ data: 'mocked' }); // 替換為 mock
svc.fetch().then(r => console.log(r.data)); // 輸出 'mocked'
總結
Object.getPrototypeOf(obj)安全且標準,是取得物件原型的首選方法。Object.setPrototypeOf(obj, proto)能在執行期動態改變原型,但因效能與可維護性考量,應謹慎使用。- 正確掌握原型概念,配合
Object.create、class、mixins等設計模式,可寫出 彈性且可讀性高 的 JavaScript 程式。 - 在實務開發中,盡量在建構階段決定原型,僅在特殊需求(如字典、策略切換、測試 mock)時才使用
Object.setPrototypeOf。
透過本文的範例與最佳實踐,你已具備在日常開發與進階專案中靈活運用原型與繼承的能力。祝你寫程式愉快,持續探索 JavaScript 更深層的奧秘!