Ch. 10 原型
[[Prototype]]
JS 的物件中有一個 [[Prototype]] 特性,表示對另一個物件的參考,預設被賦予一個非 null 值。
當取用一個物件的特性時 (例如 myObj.a),第一步就是先檢查物件是否有 a 特性。當不存在的時候,[[GET]] 便會循著物件的 [[Prototype]] 特性往上尋找。這個動作會持續進行,直到串鏈的尾端都還沒找的,就返回 undefined。
正常的串鏈尾端會是內建的 Object.prototype 物件。在此會有內建的特性與方法。
設定與遮蔽 (Shadowing) 特性
當設定一個特性到物件上時,可能發生以下三種情況,以在 myObject 加入 foo 為例:
- foo 在 myObject 被新增或是覆蓋。
- foo 在串鏈較高處被找到,而且該特性設定為唯獨,那麼設定的動作會被忽略,若在 strict mode 中,則會擲出錯誤。
- foo 在串鏈較高處被找到,該名稱是一個設值器 (setter),則會呼叫設值器,不會有 foo 被新增到 myObject。
如果在第 2、3 種情況中,想要設值給 myObject,需要改為使用 Object.defineProperty()。
隱含的遮蔽有時會帶來意想不到的結果,參考以下程式碼:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // oops, implicit shadowing!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
「類別」
JS 函式中具有一個特殊特性,公開、不可列舉,叫做 prototype,會指向一個任意的物件。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
當透過呼叫 new Foo() 產出一個物件時,a 內部的 [[Prototype]] 會連結到 Foo.prototype。
視覺上來說,[[Prototype]] 的機制如圖所示:

使用「繼承」一詞,暗示著拷貝的行為,然而在 JS 中,只是將物件建立連結,更合適的詞彙是「委派 (delegation)」。
「建構器」
Foo.prototype 預設會得到一個公開、不可列舉的特性 construct,會指向物件所關聯的函式,在這裡是 Foo。
然而 Foo 函式本身並不是一個建構器,只是剛好以 new 來呼叫時,會建構出一個物件。在查找 a.constructor 時,看似意味著 a 真的有一個實際的 constructor,但實際上只是委派給了上頭的 Foo.prototype。
.constructor 並不是一個神奇的不可變特性,它不可列舉,但值是可以寫入、可以被更改的,也可以在串鏈中的任何物件中隨意地新增。因此這個特性無法真正被信任,不可被假設為是預設值,十分地不可靠。
(Prototypal 原型式) 繼承
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// here, we make a new `Bar.prototype`
// linked to `Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );
// Beware! Now `Bar.prototype.constructor` is gone,
// and might need to be manually "fixed" if you're
// in the habit of relying on such properties!
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
透過 Object.create(..) 創建了一個新的物件,並將其內部的 [[Prototype]] 連結到指定的物件上。
常見的誤解是採用以下的方式來建立,但會帶有一些副作用:
(1) Bar.prototype = Foo.prototype;
只是建立一個共用參考,當修改 Bar.prototype.myLabel = ... 時,會影響到參考任何連結到 Foo.prototype 的物件。
(2) Bar.prototype = new Foo();
的確會建立新的物件並且正確連結,但如果函式有改變狀態、新增資料特性給 this 等等副作用行為發生時,很可能對錯的物件做這些行為。
ES6 之後,我們可以使用 Object.setPrototypeof(..) 來修改 Bar.prototype。
// pre-ES6 // throws away default existing `Bar.prototype` Bar.prototype = Object.create( Foo.prototype ); // ES6+ // modifies existing `Bar.prototype` Object.setPrototypeOf( Bar.prototype, Foo.prototype );
檢視「類別」關係
檢視物件的繼承世系 (inheritance ancestry),在傳統類別導向中,被稱為 introspection (內省)。
instanceof 運算子:用法為 [Object] instanceof [Function],這使得只有在有函式可以測試的情況下,才可以查詢某個物件的世系。若想知道 a、b 兩個物件是否有串鏈,使用此運算子就幫不上忙了。
進行反思較為合適的、乾淨的作法,是採用 isPrototypeOf(..),如 b.isPrototypeOf(c)。
物件連結
使用 Object.create(..) 建立物件的連結,是比較好的方式,幫助我們建立新的物件,並連結到指定的物件上。而沒有 new, .prototype, .constructor 等等令人困惑的參考。
在 ES5 之前的環境使用的話,需要 polyfill。
連結作為備援之用?
不要將物件連結以作為備援,考慮:
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"
設計出讓開發人員在 myObject 沒有 cool 方法的情況下使用 myObject.cool(),意味著程式中多了一些令人驚訝和不可預期的部分,請避免使用這樣的方式,或是以委派 (delegation,參閱第 11 章) 的作法來實現。
複習
- 原型串鏈 (prototype chain) 意指尋訪物件的串鏈來尋找某個特性。
- 所有正常物件的頂端都是
Object.prototype。 - 這些機制看起來很類似類別導向語言中的實體化或繼承,但是實際上在 JS 中,並不會進行拷貝的動作,只是用物件串聯彼此。