Ch. 10 原型


[[Prototype]]

JS 的物件中有一個 [[Prototype]] 特性,表示對另一個物件的參考,預設被賦予一個非 null 值。

當取用一個物件的特性時 (例如 myObj.a),第一步就是先檢查物件是否有 a 特性。當不存在的時候,[[GET]] 便會循著物件的 [[Prototype]] 特性往上尋找。這個動作會持續進行,直到串鏈的尾端都還沒找的,就返回 undefined

正常的串鏈尾端會是內建的 Object.prototype 物件。在此會有內建的特性與方法。

設定與遮蔽 (Shadowing) 特性

當設定一個特性到物件上時,可能發生以下三種情況,以在 myObject 加入 foo 為例:

  1. foo 在 myObject 被新增或是覆蓋。
  2. foo 在串鏈較高處被找到,而且該特性設定為唯獨,那麼設定的動作會被忽略,若在 strict mode 中,則會擲出錯誤。
  3. 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 中,並不會進行拷貝的動作,只是用物件串聯彼此。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *