JavaScript中的繼承

前言

作爲 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實現繼承的幾種方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章