詳解Javascript的繼承實現

1. 混合方式實現及問題

瞭解問題以前,先看看它的具體實現:javascript

從結果上來講,這種繼承實現方式沒有問題,Manager的實例同時繼承到了Employee類的實例屬性和實例方法,而且經過instanceOf運算的結果也都正確。可是從代碼組織和實現細節層面,這種方法還有如下幾個問題:html

1)代碼組織不夠優雅,繼承實現的關鍵部分的邏輯是通用的,都是以下結構:java

這段代碼缺少封裝。另外在添加子類的實例方法時,不能經過SubClass.prototype = { method1: function() {} }這種方式去設置,不然就把子類的原型整個又修改了,繼承就沒法實現了,這樣每次都得按SubClass.prototype.method1 = function() {} 的結構去寫,代碼看起來很不連續。git

解決方式:利用模塊化的方式,將通用的邏輯封裝起來,對外提供簡單的接口,只要按照約定的接口調用,就可以簡化類的構建與類的繼承。具體實現請看後面的內容介紹,暫時只能提供理論的說明。github

2)在給子類的原型設置成父類的實例時,調用的是new SuperClass(),這是對父類構造函數的無參調用,那麼就要求父類必須有無參的構造函數。但是在javascript中,函數沒法重載,因此父類不可能提供多個構造函數,在實際業務中,大部分場景下父類構造函數又不可能沒有參數,爲了在惟一的一個構造函數中模擬函數重載,只能藉助判斷arguments.length來處理。問題就是,有時候很難保證每次寫父類構造函數的時候都會添加arguments.length的判斷邏輯。這樣的話,這個處理方式就是有風險的。要是能把構造函數裏的邏輯抽離出來,讓類的構造函數所有是無參函數的話,這個問題就很好解決了。web

解決方式:把父類跟子類的構造函數所有無參化,而且在構造函數內不寫任何邏輯,把構造函數的邏輯都遷移到init這個實例方法,好比前面給出的Employee和Manager的例子就能改形成下面這個樣子:編程

用init方法來完成構造功能,就能夠保證在設置子類原型時(Manager.prototype = new Employee()),父類的實例化操做必定不會出錯,惟一很差的是在調用類的構造函數來初始化實例的時候,必須在調用構造函數後手動調用init方法來完成實際的構造邏輯:bootstrap

要是能把這個init的邏輯放在構造函數內部就行了,但是這樣的話就會違背前面說的構造函數無參無邏輯的原則。換一種方式來考慮,這個原則的目的是爲了保證在實例化父類做爲子類原型的時候,調用父類的構造函數不會出錯,那麼就能夠稍微打破一下這個原則,在類的構造函數裏添加少許的而且必定不會有問題的邏輯來解決:app

調用結果仍然和前面的例子同樣。可是這個實現還有一個小問題,它引入了一個全局變量initializing,要是能把引入這個全局變量就行了,這個其實很好解決,只要咱們把關於類的構建跟繼承,封裝成一個模塊,而後把這個變量放在模塊的內部,就沒有問題了。模塊化

3)在構造子類的時候,是把子類的原型設置成了父類的一個實例,這個是不符合語義的,繼承應該發生在類與類之間,而不是類與實例之間。之因此要用父類的一個實例來做爲子類的原型:

徹底是由於父類的這個實例,指向父類的原型,而子類的實例又會指向子類的原型,因此最終子類的實例就能經過原型鏈訪問到父類原型上的方法。這個作法雖然能實現實例方法的繼承,可是它不符合語義,並且它還有一個很大的問題就是會增長原型鏈的長度,致使子類在調用父類方法時,必須經過原型鏈的查找到父類的方法才行。要是繼承層次較深,會對js的執行性能有些影響。

解決方式:在解決這個問題以前,先想一想繼承能幫咱們解決什麼問題:從父類複用已有的實例屬性和實例方法。在javascript面向對象編程中,一直有一個原則就是,實例屬性都寫在構造函數或者實例方法裏面,實例方法寫在原型上面,也就是說類的原型,按照這個原則來講,就是用來寫實例方法的,並且是隻用來寫實例方法,那麼咱們徹底能夠在構建子類時,經過複製的方式將父類原型的全部方法所有添加到子類的原型上,不必定要把父類的一個實例設置成子類的原型,這樣就能將原型鏈的長度大大地縮短,藉助一個簡短的copy函數,咱們就能輕鬆對前面的代碼進行改造:

這麼作了之後,當調用m.toString的時候其實調用的是Manager類自身原型上的方法,而不是Employee類的實例方法,縮短了在原型鏈上查找方法的距離。這個作法在性能上有很大的優勢,但很差的是經過原型鏈維持的繼承關係其實已經斷了,子類的原型和子類的實例都沒法再經過js原生的屬性訪問到父類的原型,因此這個調用console.log(m instanceof Employee)輸出的是false。不過跟性能比起來,這個均可以不算問題:一是instanceOf的運算,幾乎在javascript的開發裏面用不到,至少我是沒碰到過;二是經過複製方式徹底可以把父類的實例方法繼承下來,這就已經達到了繼承的最大目的。

這個方法還有一個額外的好處是,解決了第2個問題最後提到的引入initializing全局變量的問題,若是是複製的話,就不須要在構建繼承關係時,去調用父類的構造函數,那麼也就沒有必要在構造函數內先判斷initializing才能去調用init方法,上面的代碼中就已經去掉了initializing這個變量的處理。

4)在子類的構造函數和實例方法內若是想要調用父類的構造函數或者方法,顯得比較繁瑣:

每次都得靠apply借用方法來處理。要是能改爲以下的調用就好用多了:

解決方式:若是要在每一個實例方法裏,都能經過this.base()調用父類原型上相應的方法,那麼this.base就必定不是一個固定的方法,須要在每一個實例方法執行期間動態地將this.base指定爲父類原型的同名方法,可以作到這個實現的方式,就只有經過方法代理了,前面的Employee和Manager的例子能夠改造以下:

經過代理的方式,就解決了在在實例方法內部經過this.base調用父類原型同名方法的問題。但是在實際狀況中,每一個實例方法都有可能須要調用父類的實例,那麼每一個實例方法都要添加一樣的代碼,顯然這會增長不少麻煩,好在這部分的邏輯也是一樣的,咱們能夠把它抽象一下,最後都放到模塊化的內部去,這樣就能簡化代理的工做。

5)未考慮靜態屬性和靜態方法。儘管靜態成員是不須要繼承的,但在有些場景下,咱們仍是須要靜態成員,因此得考慮靜態成員應該添加在哪裏。

解決方式:因爲js原生並不支持靜態成員,因此只能藉助一些公共的位置來處理。最佳的位置是添加到構造函數上:

最後的兩行輸出了正確的實例id,而這個id是經過Employee類的靜態方法生成的。在java的面向對象編程中,子類跟父類均可以定義靜態成員,在調用的時候還存在覆蓋的問題,在js裏面,由於受語言的限制,自定義的靜態成員不可能實現全面的面向對象功能,就像上面這種,可以給類提供一些公共的屬性和公共方法,就已經足夠了。

2. 指望的調用方式

從第1部分的分析能夠看出,在js裏面,類的構建與繼承,有不少通用的邏輯,徹底能夠把這些邏輯封裝成一個單獨的模塊,造成一個通用的類庫,以便在工做中有須要的時候,均可以直接拿來使用。這個類庫要求能完成咱們須要的功能(類的構建與繼承和靜態成員的添加),同時在使用時要足夠簡潔方便。在利用bootstrap的modal組件自定義alert,confirm和modal對話框這篇文章裏,我曾說過一些從組件指望的調用方式,去反推組件實現的一些觀點,當你明確你須要什麼東西時,你才知道這個東西你該怎麼去創造。本文要編寫的這個繼承組件也會採起這個方法來實現,我先用前面Employee和Manager的例子來模擬調用這個繼承庫的場景,經過預設的一些組件名稱或者接口名稱以及調用方式,來嘗試走通真實使用這個繼承庫的流程,有了這個東西,下一步我只須要根據這個要求去實現便可,模擬以下:

從模擬的結果來看,我想要的繼承庫對外提供的名稱只有Class, instanceMembers, staticMembers和extend而已,調用方式也很簡單,只要傳遞參數給Class函數便可。接下來就按照這個目標,看看如何一步步根據第一部分羅列的那些問題和解決方式,把這個庫給寫出來。

3. 繼承庫的詳細實現

根據API名稱和接口以及前面第1部分提出的問題,這個繼承庫要完成的功能有:

1)類的構建(關鍵:init方法)和靜態成員處理;

2)繼承關係的構建(關鍵:父類原型的複製);

3)父類方法的簡化調用(關鍵:父類原型上同名方法的代理)。

因此這個庫的實現,能夠按照這三點分紅三版來開發。

1)初版

在初版裏面,僅須要實現類的構架和靜態成員添加的功能便可,細節以下:

這一版核心代碼在於類的構建和靜態成員添加的部分,其它代碼僅僅提供一些提早能夠想到的賦值函數和變量(isObject, isFunction),並作一些參數合法性校驗的處理。添加靜態成員的代碼必定要在設置原型的代碼以前,不然就有原型被覆蓋的風險。有了這個版本,就能夠直接構建帶靜態成員的Employee類了:

在getId方法中之因此直接使用this就能訪問到構造函數Employee,是由於getId這個方法是添加到構造函數上的,因此當調用Employee.getId()時,getId方法裏面的this指向的就是Employee這個函數對象。

第二版在初版的基礎上,實現繼承關係的構建部分:

這一版關鍵的部分在於:

image

image

this.baseProto主要目的就是爲了讓子類的實例可以有一個屬性能夠訪問到父類的原型,由於後面的繼承方式是複製方式,會致使原型鏈斷裂。有了這一版以後,就能夠加入Manager類來演示效果了:

不過在Manager內部,調用父類的方法時仍是apply借用的方式,因此在最後一版裏面,須要把它變成咱們指望的this.base的方式,反正原理前面也已經瞭解了,無非是在方法同名的時候,對實例方法加一個代理而已,實現以下:

核心部分是:

image

只有當須要繼承父類,且父類原型中有方法與當前的實例方法同名時,纔會去對當前的實例方法添加代理。更詳細的原理能夠回到文章第1部分回顧相關內容。至此,咱們在Manager類內部調用父類的方法時,就很簡單了,只要經過this.base便可:

注意這兩處調用:

image

以上就是本文要實現的繼承庫的所有細節,其實它所作的事就是把本文第1部分提到的那些問題的解決方式和第二部分模擬的調用場景結合起來,封裝到一個模塊內部而已,各個細節的原理只要理解了第1部分總結的那些解決方式就很掌握了。在最後一版的演示中,也能看到,本文實現的這個繼承庫,已經徹底知足了模擬場景中的需求,從此有任何須要用到繼承的場景,徹底能夠拿最後一版的實現去開發。

相關文章
相關標籤/搜索