JavaScript物件導向繼承:以prototype方式繼承中需要呼叫constructor的問題
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的建構子,而且整體程式的排版更為直觀、易讀。
但是這種方法的缺點在於JavaScript IDE不支援,即使是我使用過最聰明的Aptana Studio也看不懂Dean Edwards繼承類別的運作方式,所以上圖的自動提示中是找不到類別B所繼承的方法method。
結語
其實一開始看到Dean Edwards提出這個問題時,我還看不懂這是什麼意思。直到實作了一段時間之後才發現原來建構子真的一直被重複呼叫,特別是我在系統中使用了多層次的類別繼承,導致呼叫次數多到一種難以想像的地步。
我試著用prototype的方式來挑戰嘗試迴避這個缺點,但是並沒有成功地找出解法。看來還是得用Dean Edwards的Base類別之類的迂迴手法才能解決這個問題吧?但是Base類別會讓Aptana看不懂,會影響到開發時候的效率,各有優劣,就看大家怎麼考量了。不過為了解決這個問題,我也想到一種「偽裝繼承」的方式來讓Base能夠用在Aptana Studio當中,下次有機會再來講講。