文章結構
JavaScript的繼承實現設計得有點遮遮掩掩,對於從強類型語言轉向來學習JavaScript的新手來說,是件很費腦瓜子的事情。
Sodino作爲從Java轉向JavaScript的新學員,嘗試用這篇文章來理清‘繼承’這點事。
繼承的判斷標準
考慮到JavaScript已經實現了’instanceof’這個運算符,所以本文中約定如下判斷標準:
|
|
當chlid instancof Parent
值爲true時,才判定Child
繼承自Parent
。
在此判斷標準下,來看看以下各種“百花齊放”的繼承實現方式吧…操傢伙,割韭菜。
真真假假的繼承實現方式
在各種實現方式分爲兩種思路:
增加
Child
的屬性、方法- 構造函數綁定
操作
prototype
實現繼承關係prototype
拷貝- 直接繼承
prototype
prototype
模式- 利用空對象
下面逐一細說各種方式的實現與結論判斷。
構造函數綁定
可以使用Function
的apply()
、call()
、bind()
來綁定構造函數,實現所謂的’繼承’效果。
如下代碼,child可以執行在Parent類中定義的play()
方法。
|
|
代碼運行如下:
使用構造函數綁定的方式,對於Chlid()
構造函數來說,相當於借用了Parent()
函數內的內容來對Child
進行屬性或方法的定義,在本例中是新增加了play()
方法。
與下面的代碼是等價的。
|
|
應該知道instancof
的運算原理是和對象的原型鏈相關的,所以構造函數綁定的方式並沒有將Parent
與Child
在原型鏈上建立關係。代碼運行後child instancof Parent
值是false!!!
所以這種方式只是代碼複用的一種技巧,看起來是’繼承‘,是假’繼承‘。
prototype的拷貝
這種實現方式是將Parent.prototpye
中的屬性、方法全部複製到Child
中去。
實現如下:
|
|
代碼運行如下:
很明顯,由於extendByCopy()
只是將兩個類的prototype
經複製後看起來一模一樣,但並沒有真正在Child
的原型鏈建立與Parent
的關係,所以child instanceof Parent
值仍爲false,所以這也是一種假的’繼承‘實現方法。
直接繼承prototype
直接繼承prototype
的方法是將Parent.prototype
賦值到Child.prototype
,使兩者的prototype
是一致的。
如下代碼中,Child.prototype
指向一個新對象,但由於每個prototype
都有一個constructor
屬性,指向它的構造函數,當執行了Child.prototype = Parent.prototype
後,Child.prototype.constructor
將會等於Parent
,會導致後續通過Child()
構造函數初始化的對象的constructor
都會是Parent()
,這顯然會是繼承鏈的紊亂。
所以必須手動糾正,將Child.prototype.constructor
賦值爲Child
本身,以此解決。
這也是JavaScript中務必要遵守的一點,如果替換了prototype
對象,則下一步必然是爲新的prototype
對象加上constructor
屬性並指回原來的構造函數。
代碼實現如下:
|
|
運行輸出如下圖:
這種方式看似符合文章開頭對’繼承的判斷標準’。但真的是‘繼承’嗎?很明顯該方式有以下缺點:
第一繼承關係紊亂了。
child instanceof Parent
值爲true是正常的,但parent instanceof Child
值也爲true,這…‘亂倫’的畫面感不敢看。
第二,由於示例代碼中 play()
方法並沒有聲明在Parent.prototype
中,所以Child
的對象也無法直接調用該方法。
第三,兩者的prototype
一致了,會導致對任一prototype
的改動都會同時反饋在Chlid
和Parent
上,而這是不嚴謹的編程思想。(雖然嚴謹也不是JavaScript的風格,JavaScript一直都是隨隨便便的)
第四,在debug界面查看Child
的原型鏈,發現其不完整,缺少了Parent
這一環了;而且Parent
也被指向了Child
,會導致後續調bug時干擾分析思路。
所以’直接繼承prototype’方式,雖然滿足child instanceof Parent == true
,但這種代碼技巧更像是一種‘變臉易容’而已,Sodino也把該方式歸爲假繼承。
prototype模式
prototype模式
是對上文直接繼承prototype
的改進,指將子類的prototype
對象指向一個父類的實例。
|
|
運行後代碼如下所示:
終於child instanceof Parent
值爲true了。這是一種真正的繼承實現方式。
可以在debug界面上觀察該child
對象的原型鏈如下圖所示:
相比上文的直接繼承prototype
,Parent
的原型鏈並沒有被改變,而且子類的原型鏈從Child
指向Parent
再指向Object!很完美!
利用空對象
上文prototype模式
已經完美實現繼承了。但從代碼設計層面上來看,JavaScript中,prototype
中聲明的屬性、方法是共用、共享的,這部分數據被子類是繼承是沒有問題的。
但父類也有一些自己定義的私有屬性、方法,如代碼中的play()
方法,在JavaScript語言層面上,它並沒有定義在Parent.prototype
中,所以能不能在實現繼承的同時保留該方法仍是父類的私有方法,子類不可訪問嗎?
答案是可以的。上文prototype模式
使用了Parent
的一個實例對象,由於該實例對象中有play()
方法,所以JavaScript解釋器在執行chlid.play()
時,發現child
本身並沒有定義,會順着原型鏈逐級向上查找直至找到或找不到拋出異常。在本文示例中,很方便就在Child.prototype
,即new Parent()
的這個對象中找到了該方法並執行。
所以做出的改進要保留不變的是Child.prototype
仍然通過一個對象間接指向Parent.prototype
,需要做出改變的是該對象是個空對象即可。
具體實現爲Child.prototype
指向一個空的構造函數,但該空的構造函數原型指向Parent.prototype
即可。
|
|
運行後效果如下圖。
查看child
與parent
的原型鏈,仍舊很完美。
所以這是一種更嚴格的繼承實現方式。