[[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 中,並不會進行拷貝的動作,只是用物件串聯彼此。