本文 AI 產出,尚未審核

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 為原始值(如 numberstring),會先將其包裝為對應的物件(Number、String)再返回其原型;若 objnullundefined,會拋出 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.setPrototypeOfdog 立即取得了 animaleat 方法。

範例 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 原型的物件當作常規物件 失去 toStringhasOwnProperty 等常用方法,導致意外錯誤。 只在需要「純粹字典」時使用 Object.create(null);一般物件仍保留 Object.prototype
將非物件當作第二參數 Object.setPrototypeOf 只接受物件或 null,否則拋出 TypeError 確認第二參數是 ObjectArrayFunction 等,或先使用 Object(prototype) 包裝。
混用 __proto__Object.setPrototypeOf 雖然功能相似,但 __proto__ 並非標準,可能在未來被棄用。 完全拋棄 __proto__,改用官方 API。
改變原型後忘記更新 instanceof 判斷 instanceof 依賴原型鏈,改變後舊的判斷可能失效。 改變原型後重新檢查或避免在執行期依賴 instanceof

最佳實踐

  1. 盡量在建構階段確定原型
    使用 classfunctionObject.create 先行設定好原型,避免之後再改。

  2. 使用 Object.create(proto, properties) 建立帶有自訂屬性的物件

    const proto = { greet() { console.log('Hi'); } };
    const obj = Object.create(proto, {
      name: { value: 'Tom', writable: true, enumerable: true }
    });
    
  3. 在需要「純粹」鍵值對時,選擇 Object.create(null),而不是手動 setPrototypeOfnull

  4. 檢查瀏覽器相容性:IE11 不支援 Object.setPrototypeOf,若需相容可改用 Object.__proto__(僅限舊環境)或改寫為 Object.create

  5. 避免在性能敏感的迴圈內呼叫 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.createclassmixins 等設計模式,可寫出 彈性且可讀性高 的 JavaScript 程式。
  • 在實務開發中,盡量在建構階段決定原型,僅在特殊需求(如字典、策略切換、測試 mock)時才使用 Object.setPrototypeOf

透過本文的範例與最佳實踐,你已具備在日常開發與進階專案中靈活運用原型與繼承的能力。祝你寫程式愉快,持續探索 JavaScript 更深層的奧秘!