點擊上方「前端真好玩」,喜歡他就關注他
聽說,看愷哥的文章會上癮
這是重學 JS 系列的第二篇文章,寫這個系列的初衷也是為了夯實自己的 JS 基礎。既然是重學,肯定不會從零開始介紹一個知識點,如有遇到不會的內容請自行查找資料。
原型繼承得靠原型來實現,當然原型不是這篇文章的重點,我們來複習一下即可。
其實原型的概念很簡單:
•所有對象都有一個屬性 __proto__ 指向一個對象,也就是原型•每個對象的原型都可以通過 constructor 找到構造函數,構造函數也可以通過 prototype 找到原型•所有函數都可以通過 __proto__ 找到 Function 對象•所有對象都可以通過 __proto__ 找到 Object 對象•對象之間通過 __proto__ 連接起來,這樣稱之為原型鏈。當前對象上不存在的屬性可以通過原型鏈一層層往上查找,直到頂層 Object 對象
其實原型中最重要的內容就是這些了,完全沒有必要去看那些長篇大論什麼是原型的文章,初學者會越看越迷糊。
當然如果你想了解更多原型的深入內容,可以閱讀我 之前寫的文章[1]。
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[2] 中最先更新,有興趣的可以關注下。今年主要會著重寫以下三個專欄
•重學 JS•React 進階•重寫組件
References[1] 之前寫的文章: https://github.com/KieSun/Dream/issues/2
[2] Github: https://github.com/KieSun/Dream長按掃描二維碼
我們一起好好玩
這是一個很好玩的公眾號