JavaScript開發者的繁殖速度和它的語言特性一樣迅猛,這是好事,但是也把JS搞得比任何一種其他語言都更像流行樂,充滿教派和玄學。但程式設計不是玄學,是科學和工程。這篇文章就用來闡述和探討JavaScript中的一個比較關鍵的概念,雖然在實踐上並不如在理論上那麼意義重大。
Prototype Inheritance是JavaScript裡的一個標誌性特性。實際上它叫做Inheritance是有一些問題的,JS沒有type系統,instanceof也只是一個沿著原型鏈查詢constructor的語法糖,一個物件是誰構造出來的並不說明任何問題,因為它的物件沒有結構上的穩定性承諾,只能靠程式設計師自覺。
Inheritance是OO近30年的工程實踐裡留下來的重要特性,但是它不是一個好的特性。當然好與不好是相對的,在絕大多數情況下它都不是太大的問題,尤其是工程進入尾聲,開發者對問題和模型有充分認識的時候,Class Hierarchy可以是很合理的設計。
那麼,在更General的層面上去問Inheritance設計解決的是什麼問題呢?兩個字,reuse。
在Java裡,reuse有兩個語法關鍵字,一個是extends,即inheritance,另一個是implements,實現interface。
那麼為什麼把implements也當成reuse呢?因為任何模組總有兩個方面,使用者和提供者,implements實現了一個interface,所以等價於重用了使用者程式碼。
當然你說extends也達到了同樣的目的呀?而且我還重用了父類的狀態和行為呢?是的,凡是兩面,有得有失;這正是它倒黴的地方。
它倒黴的具體情況被稱為fragile base class問題,wiki上有詞條,不贅述。
因為extends/inheritance是一種長程的關聯,基類的修改對繼承者的影響難以估計和維護,具有ripple effect特性,因此從耦合度的角度說,它大大提高了元件的耦合度,高耦合度不是罪過,但它應對變化的能力變差了。
好了,說了這麼多我們說到了問題的本原。
在Self語言中,也是最早試圖解決這個問題的語言設計者們,給出了Prototypal Inheritance設計,
它的設計初衷有兩個:
抹平Class和Object的差異,讓修改基類變得容易;
如果你修改基類,複製一個基類物件然後修改,新繼承者從新的基類物件開始繼承。其他繼承者不受影響。
你覺得這個差別很重要嗎?其實在實踐上沒有想象的那麼重要。
JavaScript在設計上還有點不同,它的原型物件是共享而非複製的,結果是隻適合把方法裝載到原型上去,偶爾有一些同類物件相同的只讀context也可以這樣做,其他每物件私有態還得通過呼叫父類建構函式做出來,即ES6裡的super關鍵字,如果是ES5,得手動把this bind到父類建構函式上呼叫。這個特性就語言而言是重要特性,但是和我們討論的問題沒太大關係。
更重要的問題出在設計上而不是語言層面。
James Gosling在一次研討會上回答問題時,有人問了他這樣一個問題,如果重新設計Java語言,他會有什麼重要的取捨。Gosling的回答可能有點兒令你吃驚:他說如果可以重新來過,他不會賦予Java語言繼承特性,只用implements。
呃?!
其實是可以理解的。
繼承設計看起來在程式碼重用上很方便,但是它的fragile base class問題,讓它無法應對軟體系統的scale問題。這一點不用論證,在整個軟體工業上,繼承這種whitebox reuse不能scale是一個定論,單一程式用繼承書寫程式碼不是問題,但是任何有點規模的系統都是靠interface,protocol,或者所謂的component-based工程方法來搭建的,也就是在更高粒度的設計層面只有基於Interface的blackbox reuse。
在任何語言中,都能用blackbox reuse構建複雜系統。在JavaScript中,龐大的npm包系統實現的最終應用,也是blackbox reuse。
JavaScript本身是否functional是洗剪吹們喜歡探討的,嚴肅的工程師不該幹這個事兒;但是整個JavaScript的Community的共識是基於blackbox reuse構建系統,這是好事。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
那麼是否該使用extends呢?
很多時候沒什麼必要性。既然Gosling都說不該去通過inheritance-based hierarchy去組合複雜行為(犧牲低耦合度),誰還比他更有資格對這個問題發言呢?
通常可以把幾個邏輯單元糖葫蘆一樣串起來實現一個從外部看來功能特別Powerful的一個物件時,也很容易把每個獨立單元用decorator,facade之類的pattern串起來,可能會多寫點兒程式碼,但不會很多,JavaScript作為無型別動態語言在書寫pattern時具有顯著的簡潔優勢。(在C++/Java裡是相反的,只寫inheritance顯著比寫pattern簡潔;但是問題就是問題你無法迴避,如果必須要鬆開耦合,還得回到pattern上定義,這也是為什麼這些pattern被髮明出來的原因。)
當然用extends方便的時候也沒必要去牴觸它,比如node裡的event emitter,stream等等,該用就用唄,只要不去試圖構造framework一樣的hierarchy即可。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果有人跟你說JavaScript不是OO語言,請一腳把它踹溝裡去,JS裡除了用Object Literal寫出來的ex nihilo物件之外,(邏輯上)所有物件都是用建構函式構造出來的,這甚至包括全域性的Object, Array,Function等等,一切皆物件是JS的最高設計思想。你怎麼能說它不是OO的?