本文 AI 產出,尚未審核

JavaScript 物件(Objects)— Object.create()

簡介

在 JavaScript 中,物件是語言的核心抽象,幾乎所有程式都會與物件互動。建立物件的方式有很多:直接使用物件字面量 ({})、建構函式 (new MyClass())、class 語法,還有一個常被忽略但非常實用的 API —— Object.create()

Object.create() 讓開發者可以 自訂原型,在不需要呼叫建構函式的情況下快速產生新物件。這對於 原型繼承、混入(mixin)以及建立純粹的資料結構 都非常有幫助。掌握它不僅能寫出更具彈性的程式碼,還能避免因過度依賴 new 而產生的副作用。

本篇文章將從概念、實作範例、常見陷阱與最佳實踐,逐步帶你深入了解 Object.create(),並提供實務上的應用情境,讓你在日常開發中即刻上手。


核心概念

1. Object.create() 的語法

Object.create(proto, [propertiesObject])
  • proto:要作為新物件原型的物件。如果想要建立一個「純淨」物件(沒有任何原型),可傳入 null
  • propertiesObject(可選):與 Object.defineProperties 相同的屬性描述子(descriptor)物件,用來一次性定義多個屬性。

重點Object.create() 只負責設定原型,不會執行任何建構函式的程式碼。


2. 原型鏈與 Object.create()

在 JavaScript 中,每個物件都有一個內部屬性 [[Prototype]](在程式中可透過 __proto__Object.getPrototypeOf 取得)。Object.create() 正是透過這個機制,把指定的原型直接掛在新物件上。

const animal = {
  speak() { console.log('...'); }
};

const dog = Object.create(animal);
dog.bark = function () { console.log('汪汪'); };

dog.speak(); // 從 animal 繼承
dog.bark();  // 自己的屬性

在上述範例中,dog[[Prototype]] 指向 animal,因此 dog 能直接呼叫 animal.speak


3. 範例一:建立「純淨」物件

有時候,我們想要一個沒有任何原型(即不會繼承 Object.prototype 的方法,如 hasOwnProperty)的物件,常用於 字典(map)安全的資料容器

const dict = Object.create(null); // 完全沒有原型
dict['apple'] = 3;
dict['banana'] = 5;

console.log(dict.hasOwnProperty); // undefined
console.log('apple' in dict);      // true

小技巧:使用 Object.create(null) 可以避免原型鏈上意外的屬性衝突,尤其在處理使用者輸入的鍵名時特別安全。


4. 範例二:使用屬性描述子一次定義多個屬性

Object.create() 的第二個參數讓我們能同時設定屬性的 enumerable、configurable、writable 等屬性描述子。

const personProto = {
  greet() {
    console.log(`哈囉,我是 ${this.name}`);
  }
};

const alice = Object.create(personProto, {
  name: {
    value: 'Alice',
    writable: false,   // 只讀
    enumerable: true,
    configurable: false
  },
  age: {
    value: 28,
    writable: true,
    enumerable: true,
    configurable: true
  }
});

alice.greet();           // 哈囉,我是 Alice
alice.age = 29;          // 可寫
// alice.name = 'Bob';   // TypeError in strict mode (不可寫)
console.log(Object.keys(alice)); // [ 'name', 'age' ]

此寫法比先 Object.create 後再逐一賦值更具可讀性,也能一次設定屬性的存取控制。


5. 範例三:模擬類別繼承(ES5 風格)

在 ES6 以前,常用 Object.create() 來實作「類別」的繼承關係。

// 基底「類別」Person
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function () {
  console.log(`Hi, 我是 ${this.name}`);
};

// 子類別 Student,繼承 Person
function Student(name, school) {
  Person.call(this, name); // 呼叫父建構函式
  this.school = school;
}
Student.prototype = Object.create(Person.prototype); // 設定原型
Student.prototype.constructor = Student; // 修正 constructor

Student.prototype.introduce = function () {
  console.log(`我是 ${this.name},就讀於 ${this.school}`);
};

const s = new Student('Tom', '國立台灣大學');
s.sayHi();        // Hi, 我是 Tom
s.introduce();    // 我是 Tom,就讀於 國立台灣大學

透過 Object.create(Person.prototype)Student 的原型指向 Person.prototype,從而得到方法繼承的效果。這也是現在 class extends 背後的實作原理。


6. 範例四:混入(Mixin)模式

Object.create() 也可以用來 快速混入 其他物件的功能,而不必改變原有原型鏈。

const canFly = {
  fly() { console.log(`${this.name} 正在飛翔!`); }
};

const canSwim = {
  swim() { console.log(`${this.name} 正在游泳!`); }
};

function createAnimal(name, abilities = []) {
  const base = { name };
  const proto = abilities.reduce((p, ability) => {
    return Object.assign(p, ability);
  }, {});
  return Object.create(proto, {
    name: { value: name, writable: true, enumerable: true }
  });
}

// 建立同時會飛又會游的動物
const duck = createAnimal('小鴨', [canFly, canSwim]);
duck.fly();   // 小鴨 正在飛翔!
duck.swim();  // 小鴨 正在游泳!

此例展示了如何把多個功能物件合併成一個「混入」原型,再產生具備所有方法的實例。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記設定 constructor 使用 Object.create() 取代原型時,constructor 會指向父原型的建構函式,導致 instanceof 判斷錯誤。 手動設定 Child.prototype.constructor = Child
屬性不可寫/不可列舉 若在第二參數中未明確設定 writableenumerable,預設為 false,屬性會變成唯讀且不可列舉。 明確寫出 { value: ..., writable: true, enumerable: true }
原型為 null 時的 instanceof Object.create(null) 產生的物件沒有原型,instanceof Object 會回傳 false,可能影響某些檢查。 只在需要「純淨」字典時使用 null,否則保留 Object.prototype
使用 __proto__ 兼容性 直接修改 __proto__ 在舊版瀏覽器或嚴格模式下不建議使用。 使用 Object.setPrototypeOf(obj, proto)(ES6)或 Object.create 重新產生物件。
混入時的名稱衝突 多個 mixin 內部屬性名稱相同,會被後加入的覆蓋。 事先規劃命名空間,或在混入前檢查 hasOwnProperty

最佳實踐

  1. 明確傳入原型:除非真的需要「無原型」物件,否則建議傳入具體的原型(如 SomeProto),保持可預測的繼承關係。
  2. 使用屬性描述子:一次性設定屬性的可寫性、列舉性與可配置性,避免日後因屬性默認為不可寫而產生錯誤。
  3. 保持原型純粹:原型應只放置共享方法,避免在原型上放置實例專屬的資料(如 this.id = ...),以免所有子物件共享同一份資料。
  4. 結合 Object.freeze:若原型不需要再被修改,可使用 Object.freeze(proto) 提升安全性與效能。

實際應用場景

  1. 建立安全的字典或快取
    使用 Object.create(null) 作為快取容器,避免 hasOwnProperty 被覆寫或意外觸發原型鏈上的屬性。

  2. 動態生成配置物件
    在大型應用中,根據使用者設定或環境變數產生不同的配置物件,Object.create(baseConfig, overrides) 能快速產生「帶有預設值」的設定。

  3. 插件系統(Plugin System)
    每個插件提供一組功能物件,主程式使用 Object.create 把插件的功能混入核心物件,保持核心與插件的低耦合。

  4. 資料模型的原型共享
    在資料驅動的 UI 框架(如 Vue、React)中,模型物件的共用方法(如 validatetoJSON)可放在原型上,實例化時只負責儲存資料,減少記憶體占用。

  5. 測試雙(Test Doubles)與 Mock
    測試時常需要建立僅有部分方法的物件,Object.create 能快速產生只實作必要介面的測試雙,避免引入完整實作的副作用。


總結

Object.create() 是 JavaScript 中 原型繼承的底層工具,它提供了:

  • 自訂原型 的彈性,讓物件可以直接繼承任意物件或完全無原型。
  • 一次性設定屬性描述子 的能力,提升屬性控制的精細度。
  • 簡潔且安全的字典/快取 實作方式。

掌握 Object.create() 後,你能在 ES5 風格的繼承、Mixin 混入、以及高效的資料結構 上寫出更乾淨、可維護的程式碼。記得遵守最佳實踐:明確設定 constructor、合理使用屬性描述子、以及在需要時選擇 null 原型以避免原型鏈汙染。

在日常開發中,從 簡單的純淨物件複雜的插件系統Object.create() 都是值得納入工具箱的利器。祝你在 JavaScript 的原型世界裡玩得開心、寫得更好!