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。 |
| 屬性不可寫/不可列舉 | 若在第二參數中未明確設定 writable、enumerable,預設為 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。 |
最佳實踐
- 明確傳入原型:除非真的需要「無原型」物件,否則建議傳入具體的原型(如
SomeProto),保持可預測的繼承關係。 - 使用屬性描述子:一次性設定屬性的可寫性、列舉性與可配置性,避免日後因屬性默認為不可寫而產生錯誤。
- 保持原型純粹:原型應只放置共享方法,避免在原型上放置實例專屬的資料(如
this.id = ...),以免所有子物件共享同一份資料。 - 結合
Object.freeze:若原型不需要再被修改,可使用Object.freeze(proto)提升安全性與效能。
實際應用場景
建立安全的字典或快取
使用Object.create(null)作為快取容器,避免hasOwnProperty被覆寫或意外觸發原型鏈上的屬性。動態生成配置物件
在大型應用中,根據使用者設定或環境變數產生不同的配置物件,Object.create(baseConfig, overrides)能快速產生「帶有預設值」的設定。插件系統(Plugin System)
每個插件提供一組功能物件,主程式使用Object.create把插件的功能混入核心物件,保持核心與插件的低耦合。資料模型的原型共享
在資料驅動的 UI 框架(如 Vue、React)中,模型物件的共用方法(如validate、toJSON)可放在原型上,實例化時只負責儲存資料,減少記憶體占用。測試雙(Test Doubles)與 Mock
測試時常需要建立僅有部分方法的物件,Object.create能快速產生只實作必要介面的測試雙,避免引入完整實作的副作用。
總結
Object.create() 是 JavaScript 中 原型繼承的底層工具,它提供了:
- 自訂原型 的彈性,讓物件可以直接繼承任意物件或完全無原型。
- 一次性設定屬性描述子 的能力,提升屬性控制的精細度。
- 簡潔且安全的字典/快取 實作方式。
掌握 Object.create() 後,你能在 ES5 風格的繼承、Mixin 混入、以及高效的資料結構 上寫出更乾淨、可維護的程式碼。記得遵守最佳實踐:明確設定 constructor、合理使用屬性描述子、以及在需要時選擇 null 原型以避免原型鏈汙染。
在日常開發中,從 簡單的純淨物件 到 複雜的插件系統,Object.create() 都是值得納入工具箱的利器。祝你在 JavaScript 的原型世界裡玩得開心、寫得更好!