原型
繼承得靠原型來實現,當然原型不是這篇文章的重點,我們來復習一下即可。
其實原型的概念很簡單:
其實原型中最重要的內容就是這些了,完全沒有必要去看那些長篇大論什么是原型的文章,初學者會越看越迷糊。
當然如果你想了解更多原型的深入內容,可以閱讀我 之前寫的文章。
ES5 實現繼承
ES5 實現繼承總的來說就兩種辦法,之前寫過這方面的內容,就直接復制來用了。
總的來說這部分的內容我覺得在當下更多的是為了應付面試吧。
組合繼承
組合繼承是最常用的繼承方式,
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = new Parent()const child = new Child(1)child.getValue() // 1child instanceof Parent // true
以上繼承的方式核心是在子類的構造函數中通過 Parent.call(this) 繼承父類的屬性,然后改變子類的原型為 new Parent() 來繼承父類的函數。
這種繼承方式優點在于構造函數可以傳參,不會與父類引用屬性共享,可以復用父類的函數,但是也存在一個缺點就是在繼承父類函數的時候調用了父類構造函數,導致子類的原型上多了不需要的父類屬性,存在內存上的浪費。
寄生組合繼承
這種繼承方式對組合繼承進行了優化,組合繼承缺點在于繼承父類函數時調用了構造函數,我們只需要優化掉這點就行了。
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true }})const child = new Child(1)child.getValue() // 1child instanceof Parent // true
以上繼承實現的核心就是將父類的原型賦值給了子類,并且將構造函數設置為子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的構造函數。
Babel 如何編譯 ES6 Class 的
為什么在前文說 ES5 實現繼承更多的是應付面試呢,因為我們現在可以直接使用 class 來實現繼承。
但是 class 畢竟是 ES6 的東西,為了能更好地兼容瀏覽器,我們通常都會通過 Babel 去編譯 ES6 的代碼。接下來我們就來了解下通過 Babel 編譯后的代碼是怎么樣的。
function _possibleConstructorReturn (self, call) { // ... return call && (typeof call === 'object' || typeof call === 'function') ? call : self; }function _inherits (subClass, superClass) { // ... subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }var Parent = function Parent () { // 驗證是否是 Parent 構造出來的 this _classCallCheck(this, Parent);};var Child = (function (_Parent) { _inherits(Child, _Parent); function Child () { _classCallCheck(this, Child); return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)); } return Child;}(Parent));
以上代碼就是編譯出來的部分代碼,隱去了一些非核心代碼,我們先來閱讀 _inherits 函數。
設置子類原型部分的代碼其實和寄生組合繼承是一模一樣的,側面也說明了這種實現方式是最好的。但是這部分的代碼多了一句 Object.setPrototypeOf(subClass, superClass),其實這句代碼的作用是為了繼承到父類的靜態方法,之前我們實現的兩種繼承方法都是沒有這個功能的。
然后 Child 構造函數這塊的代碼也基本和之前的實現方式類似。所以總的來說 Babel 實現繼承的方式還是寄生組合繼承,無非多實現了一步繼承父類的靜態方法。
繼承存在的問題
講了這么些如何實現繼承,現在我們來考慮下繼承是否是一個好的選擇?
總的來說,我個人不怎么喜歡繼承,原因呢就一個個來說。
我們先看代碼。假如說我們現在要描述幾輛不同品牌的車,車必然是一個父類,然后各個品牌的車都分別是一個子類。
class Car { constructor (brand) { this.brand = brand } wheel () { return '4 個輪子' } drvie () { return '車可以開駕駛' } addOil () { return '車可以加油' }}Class OtherCar extends Car {}
這部分代碼在當下看著沒啥毛病,實現了車的幾個基本功能,我們也可以通過子類去擴展出各種車。
但是現在出現了新能源車,新能源車是不需要加油的。當然除了加油這個功能不需要,其他幾個車的基本功能還是需要的。
如果新能源車直接繼承車這個父類的話,就出現了第一個問題 ,大猩猩與香蕉問題。這個問題的意思是我們現在只需要一根香蕉,但是卻得到了握著香蕉的大猩猩,大猩猩其實我們是不需要的,但是父類還是強塞給了子類。繼承雖然可以重寫父類的方法,但是并不能選擇需要繼承什么東西。
另外單個父類很難描述清楚所有場景,這就導致我們可能又需要新增幾個不同的父類去描述更多的場景。隨著不斷的擴展,代碼勢必會存在重復,這也是繼承存在的問題之一。
除了以上兩個問題,繼承還存在強耦合的情況,不管怎么樣子類都會和它的父類耦合在一起。
既然出現了強耦合,那么這個架構必定是脆弱的。一旦我們的父類設計的有問題,就會對維護造成很大的影響。因為所有的子類都和父類耦合在一起了,假如更改父類中的任何東西,都可能會導致需要更改所有的子類。
如何解決繼承的問題
繼承更多的是去描述一個東西是什么,描述的不好就會出現各種各樣的問題,那么我們是否有辦法去解決這些問題呢?答案是組合。
什么是組合呢?你可以把這個概念想成是,你擁有各種各樣的零件,可以通過這些零件去造出各種各樣的產品,組合更多的是去描述一個東西能干什么。
現在我們把之前那個車的案例通過組合的方式來實現。
function wheel() { return "4 個輪子";}function drvie() { return "車可以開駕駛";}function addOil() { return "車可以加油";}// 油車const car = compose(wheel, drvie, addOil)// 新能源車const energyCar = compose(wheel, drive)
從上述偽代碼中想必你也發現了組合比繼承好的地方。無論你想描述任何東西,都可以通過幾個函數組合起來的方式去實現。代碼很干凈,也很利于復用。
最后
其實這篇文章的主旨還是后面兩小節的內容,如果你還有什么疑問歡迎在評論區與我互動。
我所有的系列文章都會在我的 Github 中最先更新,有興趣的可以關注下。今年主要會著重寫以下三個專欄
以上所述是小編給大家介紹的JS繼承詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對VeVb武林網網站的支持!
新聞熱點
疑難解答