OO設計的主旨和關於它的一些話題談起來很大,但只着眼於Class的定義方式,我認爲它是JavaScript開發者嘗試解決問題的首選。因此,你可以在互聯網上找到許多不同的問題解決案例,但在我看過它們後不免有些失望——這些案例都是在某個場合下適用,而不是放之四海而皆準的通法。而我對這個話題的興趣來自於我的team在開發ThinWire Ajax Framework的影響。由於這個框架生成出對客戶端代碼的需求,才使我們“被迫”去實現可靠的、支持父類方法調用的OO模式。通過父類調用,你可以進一步依靠類的繼承特性來核心化通用代碼,從而更易於減少重複代碼,去掉客戶端代碼的壞味道。
下面羅列出了一些在我的研究過程中遇到的解決方式。最終,我沒有從中找出一個可以接收的解決方案,於是我不得不實現一個自己的解決方案,你將在本文的結尾部分看到這個方案。
然而父類調用在這裏是最重要的OO機制,因此我需要一個相應的工作模式,也正是因爲在我的觀點中原型化方式是醜陋的,所以我更需要一種更加自然地使用JavaScript定義類的方法。
More Solutions:
好吧,讓我們進入討論。正如開發者所察覺的那樣,在JS中實現基本的繼承是很容易的事,事實上有一些衆所周知的方法:
醜陋的Solution:
沒有進行父類調用的簡單繼承:
導致IE內存泄露的Solution:
這種實現方式能夠導致在IE中的內存泄漏,你應該儘量避免:
就像我在第一個實現方法中所註釋的那樣,第一個實現方法有些醜陋,但它相比引起內存泄漏的第二種方式便是首選了。
我把這兩種方法放在這裏的目的是指出你不應該使用它們。
硬性編碼的Solution:
讓我們看一下第一個例子,它採用了標準的原型化方式,但問題是:它的子類方法如何調用父類(基類)方法?下面是一些開發者嘗試並採用的方式:
一種企圖進行父類調用的“通病”:
上面的代碼是對第一段腳步進行修改後的版本,我去掉了一些註釋和空格,使你能注意到新的getId()方法和對父類的調用。你一定急於知道通過這樣對BaseClass的硬性編碼引用(hard coded reference),它是否能進行正確地調用BaseClass的方法?
一個正確的、多態的父類調用必做的事情是保證“this”引用指向當前對象實例和類方法。在這裏,看上去和它應該輸出的結果非常接近,看上去好像在SubClass中調用了BaseClass的getName()方法。你發現問題了嗎?這個問題是非常細小的,但卻很重要決不能忽視。通過使用上面的父類調用語法,BaseClass的getName()方法被調用,它返回一個字符串:包括類名和“this.getId()”的返回值。問題在於“this.getId()”應該返回2,而不是1。如果這和你所想的不同,你可以查看Java或者C#這類OO語言的多態性。
改進後的硬性編碼Solution:
你可以通過一個微小的改動來解決這個問題。
靜態(硬編碼)父類調用:
在ECMA-262 JavaScript/EcmaScript標準中,Call()方法是所有Function實例的一個成員方法,這已經被所有的主流瀏覽器所支持。JavaScript把所有的function看作對象,因此每個function都具有方法和附着其上的屬性。Call()方法允許你調用某個function,並在function的調用過程中確定“this”變量應該是什麼。JavaScript的function沒有被緊緊地綁定到它所在的對象上,所以如果你沒有顯式地使用call()方法,“this”變量將成爲function所在的對象。
另外一種方法是使用apply方法,它和call()方法類似,只在參數上存在不同:apply()方法接受參數的數組,而call()方法接受單個參數。
Douglas Crockford的Solution:
現在回溯到上面的示例,在這個示例中唯一的問題就是父類引用是直接的、硬性編寫的。它可以適用於小型的類繼承環境,但對於具有較深層次的大型繼承來講,這些直接引用非常難於維護。
那麼,有解決方法嗎?不幸的是這裏沒有簡單的解決方案。
JavaScript沒有提供對通過“隱性引用”方式調用父類方法的支持,這裏也沒有在其它OO語言中使用的“super”變量的等價物。於是,一些開發者做出了自己的解決方案,但就像我前面提到的那樣,每個解決方案都存在某種缺點。
例如,下面列出的衆多著名方法之一:JavaScript大師[ur=http://en.wikipedia.org/wiki/Douglas_Crockford]Douglas Crockford[/url]在他的《Classical Inheritance in JavaScript》中提出的方法。
Douglas Crockford的方法在多數情況下可以正常工作:
一次性支持代碼:
運行示例:
上面代碼的第一部分包括了Crockford的“inherit”和“uber”方法代碼。第二部分看上去和前面的示例很類似,除了我添加了用來演示Crockford方式所存在問題的第三層繼承關係。誠然,Crockford這位JavaScript大師的方法是我所找到的最可靠的方法之一,我很敬佩他在JavaScript編程方面做出的貢獻。但是,如果你使用三個依次繼承的類來考覈他的代碼,你將從輸出中發現這裏存在着細微的問題。
從輸出結果看,第一次調用的this.getId()返回了TopClass當前的id值“2”,但在調用SubClass和BaseClass的getName()方法時返回了“1”而不是“2”。從代碼上看, 在getName()方法中的父類調用行爲是正確的,三個類的名字都被正確地顯示出來。唯一的問題出現在this.uber("getId")這個父類調用被放入調用堆棧(call stack)時。因爲此時當前對象是一個TopClass實例,而每次調用在調用堆棧中的this.getId()都應該返回調用TopClass的getId()方法後的返回值。
而問題是TopClass的this.getId()方法通過this.uber("getId")執行了父類調用,這三次this.getId()調用中的後兩次錯誤地調用了BaseClass的getId()方法,這樣便在輸出結果中顯示了兩次“1”。正確的行爲應該是調用三次SubClass的getId()方法,在輸出結果中顯示三次“2”。大家可以通過FireFox的FireBug插件進行代碼debug進行觀察。
這是十分難以描述的現象,我不能保證我能把它解釋清楚。但是至少從上面的運行結果中可以看出它是錯誤的。
另外,Crockford的方法和其它一些方法的劣勢在於每個父類調用都需要一個額外的方法調用和額外的某種處理。這是否成爲你所面臨的問題,取決於你所使用的父類調用深度。在ThinWire項目的客戶端代碼中使用了大量的父類調用,因此父類調用的可靠性和快速性在項目中是很重要的。
我的初級Solution:
面對這樣的窘境——Crockford的方法出現問題、在互聯網上沒有找到符合要求的方法,我決定看看我自己是否可以發明一種可以滿足要求的方法。這花掉了我近一週的時間來使代碼工作並滿足各種情況,但我對它的工作情況很有信心,並且很快把它與framework集成在一起,TinWire的beta和beta2兩個版本中都使用了這些“初級設計”的代碼。
動態父類調用:
一次性支持代碼:
運行示例:
這裏是前面示例的,但是目前這種方式包括了通過“extend”方法實現的十分清晰的類定義模式和正確的父類調用語義。尤其是“extend”方法通過一箇中間function封裝了類定義中的每個方法,這個中間function在每次方法調用時首先把當前父類引用“$” 與正確的父類引用相互交換,然後把這個正確的父類引用傳遞給apply()進行方法調用,最後再將把當前父類引用“$” 與正確的父類引用交換回來。這種方式唯一的問題就是它需要一些中間function,它們會對性能產生不良影響。所以近來我重新審視了設計、完成了去掉了中間function了一種改良的方式。
改良後的Solution:
動態父類調用快速版本:
一次性支持代碼
運行示例:
這是最後的設計,它使用了JavaScript中一點鮮爲人知的特性:callee。
在任何方法執行過程中,你可以查看那些通過“arguments”數組傳入的參數,這是衆所周知的,但很少有人知道“arguments”數組包含一個名爲“callee”的屬性,它作爲一個引用指向了當前正在被執行的function,而後通過“$”便可以方便的獲得當前被執行function所在類的父類。這是非常重要的,因爲它是獲得此引用的唯一途徑(通過“this”對象獲得的function引用總是指向被子類重載的function,而後者並非全是正在被執行的function)。