:::

JavaScript物件導向繼承:以prototype方式繼承中需要呼叫constructor的問題

image

Dean Edwards認為更好的JavaScript物件導向應該有的其中一個條件是「I want to avoid calling a class’ constructor function during the prototyping phase」(在原形宣告期間,要避免去呼叫類別的建構子),這是由於JavaScript傳統的prototype繼承方式中,總是一直去呼叫父類別的建構子,才能讓子類別進行繼承。也就是說,父類別建構子裡面做過的事情,在該類別「真正派得上用場」之前,就得先做過一次,白白消耗客戶端的記憶體及運算時間。

這一篇是為了記錄究竟什麼是「原形宣告期間(prototyping phase)就呼叫建構子(constructor)」的問題,以及父類別的建構子裡面盡量不要使用太多動作的消極建議,最後簡單地介紹使用Dean Edwards的Base類別改善方法的優缺點。

JavaScript的prototype繼承方式

以下我舉一個簡單的JavaScript以prototype方式繼承的兩個類別「A」跟「B」,說明一併寫在註解裡面:

//建立類別A,以下是建構子(constructor)
function A() {
    //在畫面中輸出[Call A's constructor],表示執行A的建構子
    document.write("[Call A's constructor]");
}

//A的方法,稍後會給B繼承
A.prototype.method = function () {
    //表示執行A的方法
    document.write("[Hello, world!]");
};

//建立類別B,以下是建構子
function B() {
    //在畫面中輸出[Call B's constructor],表示執行B的建構子
    document.write("[Call B's constructor]");
}

//prototype方式的繼承。注意,此處呼叫了A類別的建構子
B.prototype = new A();    //畫面輸出[Call A's constructor]

//實體化類別B到變數b中
var b = new B();    //畫面輸出[Call B's constructor]

//呼叫由類別A繼承來的方法
b.method();    //畫面輸出[Hello, world!]

執行到最後,畫面上會輸出「[Call A's constructor][Call B's constructor][Hello, world!]」。然而理想上,程式的最後只有實體化類別B、沒有實體化類別A,因此應該只要呼叫到類別B的建構子,而不需要呼叫到類別A的建構子。但是實際上這種prototype繼承方式,卻是會在繼承時強制地呼叫類別A的建構子,造成資源的消耗。

設計建構子的心得

其實不僅僅是上述的問題,就連類別中的「物件類別(Object)」屬性也會受到JavaScript屬於傳址運作而可能遭受子類別影響的缺點(詳細請看我另一篇的討論),而必須在建構子中加入初始化屬性、並且呼叫父類別建構子。這即使在Dean Edwards繼承庫中,也會有一樣的問題。

換句話說,重複呼叫父類別建構子,似乎是難以避免的一個難關。在此前提之下,就是盡量不要在建構子中寫入太多程式、執行太多動作,最簡單的只要將物件類別的屬性初始化就好。

有些JavaScript程式設計師會在類別的建構子中宣告許多動作,但在建構子會被重複呼叫的前提下,這顯然地並不是很好的作法。舉例來說,以下就是在類別的建構子中宣告屬性與方法的JavaScript類別寫法,但是這會在每次呼叫建構子時重複地被執行、佔用記憶體,因此並不是很好的寫法。

function test() {
    var private1 = "I'm private variable 'private1'!";//私有成員變數
    this.public1 = "I'm public valiable 'public1'!";//公開成員變數
    function getPrivateFriend() {//私有成員函數
        alert(private1);
    }
    this.getPrivatePublic = function() {//公開成員函數
        getPrivateFriend();
    }
}

儘管如此,唯一令人欣慰的是,在繼承階層中最底層的子類別則就沒有上述的限制,因為他們不需要被別人所繼承,而他們通常也是系統中最常在建構子寫入大量動作的類別。

改用Dean Edwards的Base類別如何?

Dean Edwards針對這個問題提出了改良後的JavaScript物件導向繼承類別:Base (SkyDrive備份),這種方式可以避免呼叫到上層類別的建構子。

以下是上述範例中使用Dean Edwards的Base改良後的程式碼,請注意必須先引用Base.js喔。

<script type="text/javascript" src="Base.js"></script>
<script type="text/javascript">
//建立類別A,繼承自Base類別
A = Base.extend({
    //建構子
    constructor: function () {
        //在畫面中輸出[Call A's constructor],表示執行A的建構子
        document.write("[Call A's constructor]");    
    },
    method: function () {
        //表示執行A的方法
        document.write("[Hello, world!]");
    }
});

B = A.extend({
    //建構子
    constructor: function () {
        //在畫面中輸出[Call B's constructor],表示執行B的建構子
        document.write("[Call B's constructor]");
    }
});

//實體化類別B到變數b中
var b = new B();    //畫面輸出[Call B's constructor]

//呼叫由類別A繼承來的方法
b.method();    //畫面輸出[Hello, world!]
</script>

最後畫面上會輸出「[Call B's constructor][Hello, world!]」 ,由此可知他並不會呼叫到類別A的建構子,而且整體程式的排版更為直觀、易讀。

image

但是這種方法的缺點在於JavaScript IDE不支援,即使是我使用過最聰明的Aptana Studio也看不懂Dean Edwards繼承類別的運作方式,所以上圖的自動提示中是找不到類別B所繼承的方法method。

結語

其實一開始看到Dean Edwards提出這個問題時,我還看不懂這是什麼意思。直到實作了一段時間之後才發現原來建構子真的一直被重複呼叫,特別是我在系統中使用了多層次的類別繼承,導致呼叫次數多到一種難以想像的地步。

我試著用prototype的方式來挑戰嘗試迴避這個缺點,但是並沒有成功地找出解法。看來還是得用Dean Edwards的Base類別之類的迂迴手法才能解決這個問題吧?但是Base類別會讓Aptana看不懂,會影響到開發時候的效率,各有優劣,就看大家怎麼考量了。不過為了解決這個問題,我也想到一種「偽裝繼承」的方式來讓Base能夠用在Aptana Studio當中,下次有機會再來講講。