一步一步讀懂JS繼承模式

JavaScript做爲一種弱類型編程語言被普遍使用於前端的各類技術中,因爲JS中並無「類」的概念,因此js的OOP特性一直沒有獲得足夠的重視,並且有至關一部分使用js的項目中採用的都是面向過程的編程方式。可是隨着項目規模的不斷擴大,代碼量的不斷增長,這種方式會讓咱們編寫不少重複的、無用的代碼,並使得項目的擴展性、可讀性、可維護性變得脆弱。所以,js的OOP編程技巧則成爲進階的一條必經之路。javascript

開始以前
因爲js在ES6以前並無 「類」 的概念,所以咱們必需要了解這些特性(關係)。前端

  1. 在每使用function聲明一個函數的時候,咱們稱這個函數爲構造函數,js都會爲咱們自動建立一個原型對象。函數稱這個對象叫作prototype(老公),原型對象稱這個函數叫constructor(老婆)。
  2. 經過new關鍵字生成的對象就是這個函數的實例(Instance),這個實例稱原型對象爲__proto__(爸爸),同時也繼承了原型對象的稱呼constructor(孩兒他娘)。
  3. 實例可以繼承原型對象自身和繼承來的的全部屬性以及方法,一樣繼承到的constructor屬性指向構造函數。

至此,一個完整的家庭成員的關係已經構造出來了,並能夠經過new關鍵字不斷繁衍生息,後代老是能繼承先輩的屬性與方法。看如下這段代碼:java

function SomeClass(value){
    this.value = value;
}
SomeClass.prototype.protoValu = 'prototype value';

var  Instance = new SomeClass('some value');
複製代碼

這段代碼經過new關鍵字實例化了一個對象Instance,這個對象繼承了原型對象的protoValue屬性,並擁有自身的value屬性。那麼實例化的new關鍵字到底起了什麼做用呢?編程

  • 新建一個空對象o,並將函數的運行時上下文綁定爲這個對象。(使得this指向o)
  • 使得o的__proto__指向SomeClass.prototype。(emmm應該是認爸爸)
  • 執行構造函數內容,給對象o添加屬性與方法。(長出手和腳)
  • 判斷構造函數是否有return語句,若是有執行return,若是沒有則執行return o。(出生)

new關鍵字實際上起的做用就是,創造實例、繁衍後代的做用。設計模式

類式繼承

function SupperClass(value){
    this.value = value;
    this.fn = function(){
        console.log(this.value);
    }
}
SupperClass.prototype.otherValue = 'other value';
//聲明父類

function SubClass(value){
    this.subValue = value;
}
SubClass.prototype = new SupperClass("I'm supper value");
//聲明子類,並使得子類繼承自SupperClass
//以上爲聲明階段

//經過如下方式使用
var Instance = new SubClass("I'm sub value");
console.log(Instance.value);
console.log(Instance.otherValue);
console.log(Instance.subValue);
Instance.fn();
複製代碼

可是這種方式存在着一些問題:編程語言

  • 子類繼承自父類的實例,而實例化父類的過程在聲明階段,所以在實際使用過程當中沒法根據實際狀況向父類穿參。所以,這種方式的的可擴展性不理想。
  • 子類的家庭關係不完善。Instance.constructor = SupperClass,由於SubClass並無constructor屬性,因此最終會從SupperClass.prototype處繼承獲得該屬性。
  • 不能爲SubClass.prototype設置constructor屬性,該屬性會形成屬性屏蔽,致使SubClass.prototype不能正確獲取本身的constructor屬性,畢竟SubClass.prototype實際上也是SupperClass的實例。

構造函數繼承

function SupperClass(value1){
    this.xx = value1;
}
function SubClass(value1,value2){
    SupperClass.call(this,value1);
    this.xx = value2;
}

//實際使用
var Instance = new SubClass('value1','value2');
複製代碼

構造函數繼承方式的本質就是將父類的構造方法在子類的上下文環境運行一次,從而達到複製父類屬性的目的,在這個過程當中並無構造出一條完整的原型鏈。函數

雖然構造函數繼承解決了類式繼承的不能實時向父類傳參的問題,可是因爲其沒有一條完整的原型鏈,所以 子類不能繼承父類的原型屬性與原型方法 。我認爲它只是一個實現了繼承功能的一種方式,並不是真正的繼承。性能

組合式繼承--完美的繼承方式

function SupperClass(value){
    this.value = value;
    this.fn = function(){
        console.log(this.value);
    }
}
SupperClass.prototype.otherValue = 'other value';
//聲明父類

function SubClass(value1,value2){
    SupperClass.call(this,value1)
    this.subValue = value2;
}
SubClass.prototype = new SupperClass("I'm supper value");
//聲明子類,並使得子類繼承自SupperClass
//以上爲聲明階段

//經過如下方式使用
var Instance = new SubClass("I'm supper value","I'm sub value");
複製代碼

組合式繼承集合了以上兩種繼承方式的優勢,從而實現了「完美」繼承全部屬性並能動態傳參的功能。可是這種方式仍然不能補齊子類的家庭成員關係,由於SubClass.prototype仍然是父類的實例。優化

另一點,相信你們也已經發現了,整個繼承過程當中實際上調用了兩次父類的構造方法,使得SubClass.prototype與Instance都有一份父類的自有屬性/方法,這樣會形成額外的性能開銷,可是好在可以完整的實現繼承的目的了。ui

原型式繼承

原型式繼承又被成爲純潔繼承,它的重點只關注對象與對象之間的繼承關係,淡化了類與構造函數的概念,這樣能避免開發者花費過多的精力去維護類與類/類與原型之間的關係,從而將重心轉移到開發業務邏輯上面來。

var supperObj = {
    key1: 'value',
    func: function(){
        console.log(this.key1);
    }
}

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

//實際使用方法
//var Instance = new Factory(supperObj);
var Instance = Factory(supperObj);
複製代碼

原型式繼承由於只關注與對象與對象之間的關係,所以大多數都是使用工廠函數的方法生成繼承對象。在工廠函數中咱們 定義了一箇中間函數(會被釋放),並將這個函數的原型指向被繼承的對象,所以經過這個函數生成的對象的__proto__也就指向了被繼承對象。

在工廠函數內部實現繼承的方式與類式繼承實現的原理是同樣的,區別在於原型式繼承更加純淨,所以原型繼承方式具備類式繼承方式全部的缺點:

  • 沒法根據使用的實際狀況動態生成supperObj(沒法動態傳參)。
  • 雖然實現了對象的繼承,可是生成的子類尚未添加本身的屬性與方法。

同時原型繼承也有如下優勢:

  • 因爲其純潔性,開發者沒必要再去維護constructor與prototype屬性,僅僅只須要關注原型鏈。
  • 更少的內存開銷。

寄生式繼承--原型式繼承的二次封裝

在原型繼承中,每執行一次工廠函數都會從新生成一個新的中間函數F,並在函數結束時被回收,像我這種強迫症患者是不太能接受這種方式的。所幸,ES5提供了Object.create(),而且在原型式繼承,以及多繼承中起着重要的做用。在寄生式繼承中咱們會對原型繼承作一次優化。

var supperObj = {
    key1: 'value',
    func: function(){
        console.log(this.key1);
    }
}
function inheritPrototype(obj,value){
    //var subObj = Factory(obj);
    var subObj = Object.create(obj);
    subObj.name = value;
    subObj.say = function(){
        console.log(this.name);
    }
    return subObj;
}

var Instance = inheritPrototype(supperObj,'sub');
Instance.func();
Instance.say();
複製代碼

寄生式繼承實際上就是對原型式繼承的二次封裝,在此次封裝過程當中實現了根據提供的參數添加子類的自定義屬性。可是缺點仍然存在,被繼承對象沒法動態生成

由於原型式繼承是基於對象的繼承,對象是沒法接收參數的,所以要解決這個問題還要回到構造函數的問題上面來。

將類式繼承與寄生式繼承結合

function inheritPrototype(sub,sup){
    var obj = Object.create(sup.prototype);
    sub.prototype = obj;
    obj.constructor = sub;
    Object.defineProperty(obj,'constructor',{enumerable: false});
    //將constructor屬性變爲不可遍歷,避免多繼承時出現問題
}

function SupperClass(value1){
    this.supperValue = value1;
    this.func1 = function(){
        console.log(1);
    }
}
SupperClass.prototype.func2 = function(){
    console.log(this.supperValue);
}
//聲明父類

function SubClass(value2){
    this.subValue = value2;
    this.func3 = function(){
        console.log(this.subValue);
    }
}
//聲明子類

inheritPrototype(SubClass,SupperClass);
var Instance = new SubClass('sub');
console.log(Instance.supperValue);  //undefined
console.log(Instance.subValue); //sub
Instance.func1();   //Error
Instance.func2();   //undefined
Instance.func3();   //sub
複製代碼

在這種方式中,因爲obj對象並非SupperClass的實例,所以能夠與SubClass維護一個完整的關係(prototype與constructor),在維護關係的同時 必定要修改constructor的可枚舉屬性

在維護了構造函數與原型之間的完整關係的同時,也有一個致命的缺陷----因爲obj對象不是SupperClass的實例,因此在實例化子類的時候父類構造函數從未被調用過,所以 子類只能繼承到父類原型屬性與方法,沒法繼承到父類自有方法。

寄生組合繼承

寄生組合繼承就是將通過改良以後的寄生繼承與構造函數繼承方式組合,從而彌補寄生繼承沒法繼承父類自有屬性與方法的缺陷。

function SubClass(value1,value2){
    SupperClass.call(this,value1);
    this.subValue = value2;
    this.func3 = function(){
    	console.log(this.subValue);
    }
}
//聲明子類

var Instance = new SubClass('sup','sub');
複製代碼

組合以後,只用在SubClass中調用一次SupperClass的構造函數。本質上父類原型屬性與原型方法是經過原型鏈來繼承的,父類的自有方法是經過調用構造函數複製到自身實現繼承的。

寄生組合繼承不只完美的實現了屬性與方法的繼承,也避免了組合繼承產生重複屬性形成性能浪費,另外也支持建立子類時動態向父類傳參。在大型項目中合理運用這種方式實現類的繼承可以顯著提高代碼的可閱讀性,以及可擴展性。

參考

《JavaScript設計模式》 《你不知道的JavaScript》

相關文章
相關標籤/搜索