這是重學 JS 系列的第二篇文章,寫這個系列的初衷也是爲了夯實本身的 JS 基礎。既然是重學,確定不會從零開始介紹一個知識點,若有遇到不會的內容請自行查找資料。前端
繼承得靠原型來實現,固然原型不是這篇文章的重點,咱們來複習一下便可。git
其實原型的概念很簡單:github
__proto__
指向一個對象,也就是原型constructor
找到構造函數,構造函數也能夠經過 prototype
找到原型__proto__
找到 Function
對象__proto__
找到 Object
對象__proto__
鏈接起來,這樣稱之爲原型鏈。當前對象上不存在的屬性能夠經過原型鏈一層層往上查找,直到頂層 Object
對象其實原型中最重要的內容就是這些了,徹底沒有必要去看那些長篇大論什麼是原型的文章,初學者會越看越迷糊。面試
固然若是你想了解更多原型的深刻內容,能夠閱讀我 以前寫的文章。數組
ES5 實現繼承總的來講就兩種辦法,以前寫過這方面的內容,就直接複製來用了。瀏覽器
總的來講這部分的內容我以爲在當下更多的是爲了應付面試吧。架構
組合繼承是最經常使用的繼承方式,app
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() // 1
child 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() // 1
child instanceof Parent // true
複製代碼
以上繼承實現的核心就是將父類的原型賦值給了子類,而且將構造函數設置爲子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的構造函數。
爲何在前文說 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 中最早更新,有興趣的能夠關注下。今年主要會着重寫如下三個專欄
最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。