JavaScript中的繼承

遊蕩de蝌蚪發表於2019-05-05

前言

作為 JavaScript 中最重要的內容之一,繼承問題一直是我們關注的重點。那麼你是否清晰地知道它的原理以及各種實現方式呢

閱讀這篇文章,你將知道:

  • 什麼是繼承
  • 實現繼承有哪幾種方式
  • 它們各有什麼特點

這裡預設你已經清楚的知道建構函式、例項和原型物件之間的關係,如果並不是那麼清晰,那麼推薦你先閱讀這篇文章 -- JavaScript 中的原型與原型鏈

如果文章中有出現紕漏、錯誤之處,還請看到的小夥伴多多指教,先行謝過

以下↓

概念

繼承(inheritance)是物件導向軟體技術當中的一個概念。如果一個類別 B 繼承自 另一個類別 A ,就把這個 B 稱為 A的子類 ,而把 A 稱為 B的父類別 也可以稱 A是B的超類 。繼承可以使得子類具有父類別的各種屬性和方法,而不需要再次編寫相同的程式碼 ...更多

image

通過這些概念和圖示我們不難知道繼承可以在我們的開發中帶來的便捷,那麼在 JavaScript 中如何去實現繼承呢?

繼承實現方式

原型鏈繼承

利用原型讓一個引用型別繼承另一個引用型別的屬性和方法

function SuperType() {
    this.name = 'tt';
}
SuperType.prototype.sayName = function() {
    return this.name
}

function SubType() {
    this.name = 'oo';
}
SubType.prototype = new SuperType()

var instance = new SubType()

instance.sayName() // oo
instance instanceof SubType // true
instance instanceof SuperType // ture
複製程式碼

以上的試驗中,我們建立了兩個建構函式 SuperTypeSubType ,並且讓 SubType 的原型指向 SuperTypeSubType 也就繼承了 SuperType 原型物件中的方法。所以在建立 instance 例項的時候,例項本身也就具有了 SuperType 中的方法,並且都處在它們的原型鏈中

SubType.prototype.constructor == SubType // false
SubType.prototype.constructor == SuperType // true
複製程式碼

需要注意的是:這個時候 SubType.prototype.constructor 是指向 SuperType 的,相當於重寫了 SubType 的原型物件。

用一張圖表示:

image

  • SubType.prototype 相當於 SuperType 的例項存在的,所以 SubType.prototype.constructor 就指向 SuperType

原型繼承的特點

優點:

  • 簡單、易於實現
  • 父類新增原型方法/原型屬性,子類都能訪問到
  • 非常純粹的繼承關係,例項是子類的例項,也是父類的例項

缺點:

  • 無法實現多繼承
  • 想要為子類 SubType新增原型方法,就必須在 new SuperType 之後新增(會覆蓋)
  • 來自原型物件的所有屬性被所有例項共享(引用型別的值修改會反映在所有例項上面)
  • 建立子類例項時,無法向父類建構函式傳參

借用建構函式

在子類建構函式的內部呼叫超型別建構函式,通過 applycall 實現

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'orange', 'black'];
}

function SubType() {
    SuperType.call(this, 'tt');
}

var instance = new SubType()
var instance1 = new SubType()

instance.colors // ['red', 'orange', 'black']
instance.name // tt

instance.colors.push('green');
instance.colors // ['red', 'orange', 'black', 'green']
instance1.colors // ['red', 'orange', 'black']
複製程式碼

借用建構函式的特點

優點:

  • 解決了原型鏈繼承不能傳參的問題
  • 子類例項共享父類引用屬性的問題
  • 可以實現多繼承(call可以指定不同的超類)

缺點:

  • 例項並不是父類的例項,只是子類的例項
  • 只能繼承父類的例項屬性和方法,不能繼承原型屬性/方法
  • 無法實現函式複用

組合繼承

偽經典繼承(最常用的繼承模式):將原型鏈和借用建構函式的技術組合到一起。使用原型鏈實現對原型屬性和方法的繼承,通過建構函式來實現對例項屬性的繼承

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'orange', 'black'];
}
SuperType.prototype.sayName = function() {
    return this.name
}

function SubType() {
    SuperType.call(this, 'tt');
    this.name = 'oo';
}
// 這裡的 SubType.prototype.constructor 還是指向 SuperType
SubType.prototype = new SuperType();

var instance = new SubType();
var instance1 = new SubType();

instance.name // oo
instance.sayName() // oo

instance.colors.push('green');
instance.colors // ['red', 'orange', 'black', 'green']
instance1.colors // ['red', 'orange', 'black']
複製程式碼

組合繼承的特點

優點:

  • 可以繼承例項屬性/方法,也可以繼承原型屬性/方法
  • 不存在引用屬性共享問題
  • 可傳參
  • 函式可複用

缺點:

  • 呼叫了兩次父類建構函式,生成了兩份例項(子類例項將子類原型上的那份遮蔽了)

原型式繼承

藉助原型鏈可以基於已有的物件建立新物件,同時還不必因此建立自定義型別

function obj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

var person = {
    name: 'tt',
    age: 18,
    colors: ['red', 'green']
}

var instance = obj(person);
var instance1 = obj(person);
instance.colors.push('black');
instance.name // tt
instance.colors // ['red', 'green', 'black']
instance1.colors // ['red', 'green', 'black']
複製程式碼

建立一個臨時的建構函式,然後將傳入的物件當做這個建構函式的原型物件,最後返回這個臨時建構函式的新例項。實際上,就是對傳入的物件進行了一次淺複製

ES5 通過新增 Object.create() 規範化了原型式繼承

更多 Object.create()語法請點選 這裡

原型式繼承特點

優點:

  • 支援多繼承(傳入的物件不同)
  • 不需要興師動眾的建立很多建構函式

缺點: 和原型鏈繼承基本一致,效率較低,記憶體佔用高(因為要拷貝父類的屬性)

寄生式繼承

建立一個僅用於封裝繼承過程的函式,在函式內部對這個物件進行改變,最後返回這個物件

function createAnother(obj) {
    var clone = Object(obj);
    clone.sayHi = function() {
        alert('Hi');
    }
    return clone
}

var person = {
    name: 'tt',
    age: 18,
    friends: ['oo', 'aa', 'cc'],
    sayName() {
        return this.name
    }
}

var instance = createAnother(person)
var instance1 = createAnother(person)

instance.friends.push('yy')

instance.name // 'tt'
instance.sayHi() // Hi
instance.friends // ["oo", "aa", "cc", "yy"]
instance1.friends // ["oo", "aa", "cc", "yy"]
複製程式碼

寄生式繼承的特點

優點:

  • 支援多繼承

缺點:

  • 例項並不是父類的例項,只是子類的例項
  • 不能實現複用(與建構函式相似)
  • 例項之間會互相影響

寄生組合繼承

借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。通過寄生方式,砍掉父類的例項屬性,這樣,在呼叫兩次父類的構造的時候,就不會初始化兩次例項方法/屬性,避免的組合繼承的缺點

function inherit(subType, superType) {
    var obj = Object(superType.prototype); // 建立物件
    obj.constructor = subType;  // 指定constructor
    subType.prototype = obj;    // 指定物件
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'orange', 'black'];
}
SuperType.prototype.sayName = function() {
    return this.name
}

function SubType() {
    SuperType.call(this, 'tt');
    this.name = 'oo';
}

inherit(SubType, SuperType)

var instance = new SubType()

instance.name // oo
instance.sayName // oo
instance instanceof SubType // true
instance instanceof SuperType // true
SubType.prototype.constructor == SubType // true
複製程式碼

寄生組合繼承的特點

堪稱完美,只是實現稍微複雜一點

後記

作為 JavaScript 最重要的概念之一,對於繼承實現的方式方法以及它們之間的差異我們還是很有必要了解的。

在實現繼承的時候,拷貝 也是一種很有效的方式,由於 JavaScript 簡單資料型別與引用型別的存在,衍生出了 淺拷貝深拷貝 的概念,那麼它們又是什麼,怎麼去實現呢

且聽下回分解,哈哈

週末愉快

最後,推薦一波前端學習歷程,不定期分享一些前端問題和有意思的東西歡迎 star 關注 傳送門

參考文件

JavaScript 高階程式設計

JavaScript實現繼承的幾種方式

相關文章