本章重點#
- 探索 prototype 的運作原理
- 使用函式作為建構器 (constructor)
- 透過 prototype 擴展物件
- 避免常見陷阱
- 使用繼承 (inheritance) 建構類別階層
7.1 理解 Prototype#
在 JavaScript 中,物件是具名屬性 (named properties) 與值的集合。物件屬性可以是簡單值、函式或其他物件,而且可以隨時動態新增、修改和刪除。
Prototype 的核心概念:
- 每個物件都可以擁有一個對另一個物件的參考,稱為它的 prototype
- 當搜尋某個屬性時,如果物件本身沒有該屬性,就會委派 (delegate) 給它的 prototype
- Prototype 本身也可以有自己的 prototype,形成 prototype chain(原型鏈)
- 搜尋會沿著原型鏈一路向上,直到找到屬性或沒有更多 prototype 可搜尋為止

Figure 7.1: 初始時每個物件只能存取自己的屬性
const yoshi = { skulk: true };
const hattori = { sneak: true };
const kuma = { creep: true };
Object.setPrototypeOf(yoshi, hattori);
Object.setPrototypeOf(hattori, kuma);
// yoshi 可以存取 hattori 的 sneak 和 kuma 的 creep
assert("sneak" in yoshi); // true
assert("creep" in yoshi); // true
物件的 prototype 屬性是內部屬性(標記為
[[prototype]]),無法直接存取。使用內建的Object.setPrototypeOf方法來設定原型。

Figure 7.2: 存取物件沒有的屬性時,會搜尋其原型鏈
7.2 物件建構與 Prototype#
建構函式與 new 運算子#
每個函式在建立時都會自動獲得一個 prototype 屬性。當使用 new 運算子呼叫函式時:
- 建立一個新的空物件
- 將新物件的
[[prototype]]設定為建構函式的prototype屬性 - 函式以新物件為 context(
this)執行 - 回傳新物件
function Ninja() {}
Ninja.prototype.swingSword = function () {
return true;
};
const ninja = new Ninja();
assert(ninja.swingSword()); // true — 透過 prototype 存取

Figure 7.4: 每個函式建立時都會獲得一個新的 prototype 物件
實例屬性 (Instance Properties)#
在建構函式中透過 this 設定的屬性會直接建立在實例上。當實例屬性與 prototype 屬性同名時,實例屬性優先。
function Ninja() {
this.swung = false;
this.swingSword = function () {
// 實例方法
return !this.swung;
};
}
Ninja.prototype.swingSword = function () {
// prototype 方法
return this.swung;
};
const ninja = new Ninja();
assert(ninja.swingSword()); // true — 呼叫的是實例方法
每個實例都會有自己的方法副本,這會消耗更多記憶體。因此建議將方法定義在 prototype 上,讓所有實例共享,除非需要透過閉包模擬私有變數。

Figure 7.5: 實例屬性優先於原型屬性
JavaScript 動態特性的副作用#
JavaScript 中幾乎所有東西都可以在執行期間修改,包括 prototype:
- 在物件建立之後才新增到 prototype 的方法,既有實例仍然可以存取(因為實例持有對 prototype 物件的參考)
- 若整個 prototype 被替換為新物件,既有實例仍然參考舊的 prototype,新建立的實例才會參考新的 prototype

Figure 7.9: 函式的 prototype 可隨時替換,但已建構的實例仍引用舊原型
透過建構器進行物件分型 (Object Typing)#
typeof對所有物件都回傳"object",用處有限instanceof可判斷物件是否由特定建構函式建立constructor屬性指向建立該物件的建構函式,可用於型別檢查,甚至用來建立新實例
function Ninja() {}
const ninja = new Ninja();
assert(ninja instanceof Ninja); // true
assert(ninja.constructor === Ninja); // true
// 透過 constructor 建立新實例
const ninja2 = new ninja.constructor();
assert(ninja2 instanceof Ninja); // true
7.3 實現繼承 (Inheritance)#
繼承的關鍵技巧是將子類別的 prototype 設定為父類別的實例:

Figure 7.12: 未正確設定繼承時的測試結果
function Person() {}
Person.prototype.dance = function () {};
function Ninja() {}
Ninja.prototype = new Person(); // 關鍵:建立原型鏈
const ninja = new Ninja();
assert(ninja instanceof Ninja); // true
assert(ninja instanceof Person); // true
assert(typeof ninja.dance === "function"); // true — 繼承了 dance
不要直接使用
Ninja.prototype = Person.prototype,因為這樣對 Ninja prototype 的修改也會影響 Person prototype(它們是同一個物件)。

Figure 7.13: 透過將 Ninja 的 prototype 設為 Person 實例來實現繼承
constructor 屬性被覆蓋的問題#
當設定 Ninja.prototype = new Person() 時,原本的 Ninja prototype(包含 constructor 屬性)會被丟棄。此時 ninja.constructor 會沿著原型鏈找到 Person,而非 Ninja。
解決方法:使用 Object.defineProperty 重新定義 constructor:
Object.defineProperty(Ninja.prototype, "constructor", {
enumerable: false,
value: Ninja,
writable: true,
});設定
enumerable: false可以避免constructor出現在for-in迴圈中,維持與原生行為一致。
instanceof 運算子#
instanceof 的實際語義是檢查右側函式的 prototype 是否在左側物件的原型鏈中,而非嚴格判斷物件是否由該函式建立。
如果在建立物件之後修改了建構函式的 prototype,
instanceof的結果可能會改變,因為它檢查的是當前的 prototype 參考。
7.4 ES6 的 JavaScript “類別”#
ES6 引入了 class 關鍵字,雖然底層仍然基於 prototype 繼承,但提供了更優雅的語法糖 (syntactic sugar)。
使用 class 關鍵字#
class Ninja {
constructor(name) {
this.name = name;
}
swingSword() {
return true;
}
}
var ninja = new Ninja("Yoshi");
assert(ninja instanceof Ninja);
assert(ninja.name === "Yoshi");
assert(ninja.swingSword());上述 ES6 class 等同於以下 ES5 程式碼:
function Ninja(name) {
this.name = name;
}
Ninja.prototype.swingSword = function () {
return true;
};靜態方法 (Static Methods):使用 static 關鍵字定義類別層級的方法,只能透過類別本身存取,實例無法存取。
class Ninja {
constructor(name, level) {
this.name = name;
this.level = level;
}
static compare(ninja1, ninja2) {
return ninja1.level - ninja2.level;
}
}實現繼承#
使用 extends 關鍵字繼承另一個類別,並使用 super 呼叫父類別的建構函式:
class Person {
constructor(name) {
this.name = name;
}
dance() {
return true;
}
}
class Ninja extends Person {
constructor(name, weapon) {
super(name); // 呼叫父類別建構函式
this.weapon = weapon;
}
wieldWeapon() {
return true;
}
}
var ninja = new Ninja("Yoshi", "Wakizashi");
assert(ninja instanceof Ninja); // true
assert(ninja instanceof Person); // true
assert(ninja.name === "Yoshi");
assert(ninja.dance()); // 繼承自 Person
使用 ES6
class與extends語法,不需要手動處理 prototype 指定、constructor屬性修復等繁瑣工作,大幅簡化繼承的實作。

Figure 7.15: ES6 class 繼承的屬性與原型鏈結構
7.5 本章總結#
- JavaScript 物件是具名屬性與值的簡單集合
- 每個物件都可以參考一個 prototype,當物件本身沒有某屬性時,會委派給 prototype 搜尋,形成原型鏈
- 可透過
Object.setPrototypeOf設定物件的 prototype - 每個函式都有
prototype屬性,會自動成為其建構實例的 prototype prototype物件的constructor屬性指向函式本身,可用於判斷物件的建構來源- JavaScript 中幾乎所有東西都可以在執行期間修改,包括物件和函式的 prototype
- 要實現繼承,將子類別的 prototype 設定為父類別的新實例
- 屬性具有
configurable、enumerable、writable等特性,可用Object.defineProperty設定 - ES6 的
class關鍵字提供語法糖,底層仍是 prototype 繼承 extends關鍵字讓繼承變得優雅簡潔