重學 JS 系列:聊聊繼承

yck發表於2019-04-01

這是重學 JS 系列的第二篇文章,寫這個系列的初衷也是為了夯實自己的 JS 基礎。既然是重學,肯定不會從零開始介紹一個知識點,如有遇到不會的內容請自行查詢資料。

原型

繼承得靠原型來實現,當然原型不是這篇文章的重點,我們來複習一下即可。

其實原型的概念很簡單:

  • 所有物件都有一個屬性 __proto__ 指向一個物件,也就是原型
  • 每個物件的原型都可以通過 constructor 找到建構函式,建構函式也可以通過 prototype 找到原型
  • 所有函式都可以通過 __proto__ 找到 Function 物件
  • 所有物件都可以通過 __proto__ 找到 Object 物件
  • 物件之間通過 __proto__ 連線起來,這樣稱之為原型鏈。當前物件上不存在的屬性可以通過原型鏈一層層往上查詢,直到頂層 Object 物件

其實原型中最重要的內容就是這些了,完全沒有必要去看那些長篇大論什麼是原型的文章,初學者會越看越迷糊。

當然如果你想了解更多原型的深入內容,可以閱讀我 之前寫的文章

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() // 1
child instanceof Parent // true
複製程式碼

以上繼承的方式核心是在子類的建構函式中通過 Parent.call(this) 繼承父類的屬性,然後改變子類的原型為 new Parent() 來繼承父類的函式。

這種繼承方式優點在於建構函式可以傳參,不會與父類引用屬性共享,可以複用父類的函式,但是也存在一個缺點就是在繼承父類函式的時候呼叫了父類建構函式,導致子類的原型上多了不需要的父類屬性,存在記憶體上的浪費。

重學 JS 系列:聊聊繼承

寄生組合繼承

這種繼承方式對組合繼承進行了優化,組合繼承缺點在於繼承父類函式時呼叫了建構函式,我們只需要優化掉這點就行了。

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
複製程式碼

以上繼承實現的核心就是將父類的原型賦值給了子類,並且將建構函式設定為子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的建構函式。

重學 JS 系列:聊聊繼承

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
  • React 進階
  • 重寫元件

最後,覺得內容有幫助可以關注下我的公眾號 「前端真好玩」咯,會有很多好東西等著你。

重學 JS 系列:聊聊繼承

相關文章