close

JavaScript的繼承是透過原型鏈(prototype chain)的方式達成,所有的物件都是繼承自Object,包括代表函式的Function也是。當取用特定物件的屬性時,會先從物件的範圍開始搜尋,如果沒有,則會搜尋prototype,若沒有繼承任何類別,則prototype會指向Object,再從Object的範圍裡搜尋。以下程式定義了Person做為父類別,而Man與Woman都是Person的子類別:

var Person = function(){
    this.name   = 'Person';
    this.gender = 'Unknown';
};
Person.prototype.getName = function(){
    return this.name;
};
Person.prototype.getGender = function(){
    console.info(this);
    return 'Person:'+this.gender;
};
Person.prototype.getDNA = function(){
    return 'DNA';
};

var Man = function(){};
Man.prototype = new Person();
Man.prototype.getGender = function(){
    return 'Male';
};

var Woman = function(){};
Woman.prototype = new Person();
Woman.prototype.getGender = function(){
    return 'Female';
};

var joe = new Man();
joe.name = 'Joe';
console.info(joe.getName(), joe.getGender());
console.info(joe.getDNA());
console.info(joe.__proto__.__proto__.getGender.apply(joe));
console.info(joe.__proto__.__proto__.getGender());

第一行console.info會顯示出:Joe Male、第二行會顯示:DNA,這都在預期之內,但是比較會令人困惑的是在最後兩行console.info,其中第三行會顯示:Person:Unknown,也就是取用Person的屬性與方法,但第四行卻會顯示:Person:undefined,這是因為這兩個函式執行的範圍(scope)不一樣,第三行的範圍是joe這個物件,而第四行的範圍則是Object(若使用jsFiddle則會是window)。這裡牽扯到兩項議題:

第一,當取用屬性時,JavaScript的runtime會先從這個物件內部的屬性開始找,也就是joe本身,假設要取用getName(),因為joe沒有getName()函式,所以runtime會再從joe的prototype裡找,而getName()剛好就在joe的prototype裡,所以runtime就會調用這支函式並且執行,所以調用getName()的方式可以寫成:

joe.prototype.getName();

joe.getName();

部分的runtime裡會有定義prototype的別名:__proto__,所以又可以寫成:

joe.__proto__getName();

在Java與C++的命名建議中,通常會把私有屬性(private attritube)的名稱前使用底線(_)來標記,所以可以知道__proto__事實上就是代表一個私有的屬性,但不是所有的runtime都支援的。

如之前所提,JavaScript的繼承是透過原型鏈達成的,所以若要調用父類別的方法,可以透過prototype往回搜尋,調用Person的寫法就變成了:

joe.prototype.prototype.getGender();//or

joe.__proto__.proto__.getGender();

 不過這裡就衍生出了第二個議題:函式的執行範圍,JavaScript中的this,是指向調用函式的物件,可以透過apply()或是call()來變換函式調用時的範圍,在第四行調用getGender()時,範圍是Object物件,因為透過__proto__取用時,等於直接指示runtime調用的路徑,第一次使用__proto__時,取用到的是被覆寫過的Person物件,所以若使用console.dir()檢視joe.__proto__就會發現是Person的物件,但是因為Man把getRender()覆寫了,所以joe.__proto__.getGender()會取得用的是Man定義的getGender(),若沒有覆寫,取用的就是Person的getGender(),第二個__proto__就會取用Object的getGender方法了,因為Person沒有繼承任何物件,所以runtime會自動把Object指定給Person的prototype,而Person.prototype就等於直接操作Object物件了,joe.__proto__.proto__就是將函式調用的範圍指向了Object,而在Object裡並沒有定義gender,所以this.gender才會是undefined。透過apply可以將範圍從新定義到joe上,而不是Object上,這個時候this.gender就會是Person定義的gender。

為了避免這樣令人困惑的部分,呼叫父類別的函式時最好還是在子類別裡的函式裡使用:

Man.prototype.getGender = function(){
   console.info(this.__proto__.__proto__.getGender.apply(this));
   return 'Male';
};
arrow
arrow
    文章標籤
    javascript OOP MVC
    全站熱搜

    鍾協良 發表在 痞客邦 留言(0) 人氣()