JavaScript設計模式(2)——多種繼承方式的實現及原理

原文出自於本人個人博客網站:https://www.dzyong.com(歡迎訪問)

轉載請註明來源: 鄧佔勇的個人博客 - 《JavaScript設計模式(2)—— 多種繼承方式的實現及原理

本文鏈接地址: https://www.dzyong.com/#/ViewArticle/87

設計模式系列博客:JavaScript設計模式(1)——面對對象的編程

 

什麼是繼承

繼承面向對象軟件技術當中的一個概念。如果一個類別A“繼承自”另一個類別B,就把這個A稱爲“B的子類別”,而把B稱爲“A的父類別”也可以稱“B是A的超類”。繼承可以使得子類別具有父類別的各種屬性和方法,而不需要再次編寫相同的代碼。在令子類別繼承父類別的同時,可以重新定義某些屬性,並重寫某些方法,即覆蓋父類別的原有屬性和方法,使其獲得與父類別不同的功能。另外,爲子類別追加新的屬性和方法也是常見的做法。 一般靜態的面向對象編程語言,繼承屬於靜態的,意即在子類別的行爲在編譯期就已經決定,無法在執行期擴充

瞭解了什麼是繼承後,接下來看一下在JavaScript實現繼承的6種方式

子類的原型對象——類式繼承

let superClass = function(){
        this.superVal = true
        this.books = ['a', 'b', 'c']
    }
    superClass.prototype.getSuperVal = function(){
        return this.superVal
    }
    let sup = new superClass()
    //聲明子類1
    let subClass = function(){
        this.subVal = false
    }
    subClass.prototype = new superClass()
    subClass.prototype.getSuperVal = function(){
        this.subVal
    }

類式繼承就是聲明兩個類,只不過是把第一個類的實例賦值給第二個類的原型。

類的原型對象的作用就是爲類的原型添加共有的方法,但類不能直接訪問這些屬性和方法,必須通過原型prototype來訪問。而我們實例化一個父類的時候,新創建的對象複製了父類的構造函數與方法並將原型_proto_指向了父類的原型對象,這樣就擁有了父類原型對象上的屬性和方法,並且這個新創建的對象可直接訪問到父類原型對象上的屬性與方法。

let sub = new subClass1()
console.log(sub.superVal)   //true
console.log(sub.getSuperVal)   //false

我們可以使通過instanceof來檢測某個對象是否是某個類的實例,instanceof是通過判斷對象的prototype鏈來確定關係的,而不關心對象與類的自身結構。

console.log(sub instanceof superClass)    //true
console.log(sub instanceof subClass)    //true
console.log(subClass instanceof superClass)    //false

一定要注意:instanceof是判斷前面的對象是否是後面類(對象)的實例,並不表示兩者的繼承關係。

console.log(subClass.prototype instanceof superClass)    //true

Object是所有對象的祖先

console.log(sub instanceof Object)    //true

但是類式繼承有兩個缺點:(1)由於子類是通過其原型prototype對父類的實例化,繼承了父類。所以說父類中的共有屬性要是引用類型,就會在子類被所有實例共用,因此一個子類的實例更改子類原型從父類構造函數中繼承來的共有屬性就會直接影響到其他子類。

let sub1 = new subClass()
let sub2 = new subClass()
console.log(sub1.books)   //['a', 'b', 'c']
sub2.books.push('d')
console.log(sub1.books)   ////['a', 'b', 'c', 'd']

sub2的修改影響到了sub1的book屬性。(2)由於子類實現繼承是靠prototype對父類的實例化實現的,因此在創建父類的時候,是無法向父類傳遞參數的,因而是實例化父類的時候也無法對父類構造函數內的屬性進行初始化。

創建即繼承——構造函數繼承

let superClass = function(id){
    this.books = ['JavaScript', 'html', 'css']
    this.id = id
}
superClass.prototype.showBooks = function(){
    console.log(this.books)
}
let subClass = function(id){
    superClass.call(this, id)
}
let instance1 = new subClass(10)
let instance2 = new subClass(11)
instance1.books.push('設計模式')
console.log(instance1.books)  //["JavaScript", "html", "css", "設計模式"]
console.log(instance2.books)  //["JavaScript", "html", "css"]

call這個方法可以更改函數的作用環境,對superClass調用這個方法就是將子類中的變量在父類中執行一遍,由於父類中是給this綁定屬性的,因此子類自然就繼承了父類的共有屬性。

這種類型的繼承沒有涉及原型prototype,所以父類的原型方法自然不會被子類繼承,而如果想被子類繼承就必須要放在構造函數中,這樣創建出來的每個實例都會單獨擁有一份而不能共用,這樣就違背了代碼複用原則。爲了綜合這兩種模式的有點,後來有了組合式繼承。

將優點爲我所用——組合繼承

組合繼承顧名思義就是將上面講的兩種繼承方式組合起來用,綜合各自的優點。

let supClass = function(name){
    this.name = name
    this.books = ['JavaScript', 'html', 'css']
}
supClass.prototype.showBooks = function(){
    console.log(this.books)
}
let subClass = function(name, time){
    this.time = time
    supClass.call(this, name)
}
subClass.prototype = new supClass()
let instance1 = new subClass('dzy', 2018) 
let instance2 = new subClass('hxy', 2019) 
console.log(instance1.name, instance1.time)   //dzy 2018
console.log(instance2.name, instance2.time)   //hxy 2019
instance1.books.push('設計模式')
instance1.showBooks()   //["JavaScript", "html", "css", "設計模式"]
instance2.showBooks()   //["JavaScript", "html", "css"]

在子類構造函數中執行父類構造函數,在子類原型上實例化父類就是組合模式,這樣就融合了類式繼承和構造函數繼承的優點。

潔淨的繼承者——原型式繼承

藉助原型prototype可以根據已有的對象創建一個新的對象,同時不必創建新的自定義對象類型。

                                                                                                  —— 道格拉斯·克羅克福德《JavaScript中原型式繼承》

let inheritObject = function(o){
    //聲明一個過渡函數對象
    function F(){}
    //過渡對象的原型繼承父對象
    F.prototype = o
    //返回過渡對象的一個實例,該實例的原型繼承了父對象
    return new F()
}

它是對類式繼承的一個封裝,其中的過渡對象就相當於類式繼承中的子類,只不過在原型式中作爲一個過渡對象出現,目的是爲了創建要返回的新的實例化對象。

如虎添翼——寄生式繼承

let book = {
    name: 'js book',
    alikeBook: ['JavaScript', 'html', 'css']
}
let createBook = function(obj){
    //通過原型繼承方式創建新對象
    var o = new inheritObject(obj)
    //拓展新對象
    o.getName = function(){
        console.log(name)
    }
    return o
}

寄生式繼承就是對原型繼承的第二次封裝,並在第二次封裝過程中對繼承的對象進行了拓展,這樣新創建的對象不僅僅有父類中的屬性和方法而且還添加新增屬性和方法。

終極繼承者——寄生組合式繼承

在上面講到過類式繼承同構造函數繼承組合使用,但是有一個問題,就是子類不是父類的實例,所以纔有了寄生組合式繼承。

寄生是寄生式繼承,依託於原型繼承,原型繼承與類式繼承相像。

let inheritProject = function(subClass, superClass){
    //複製一份父類的原型副本保存到變量中
    var p = inheritProject(superClass.prototype)
    //修正因爲重寫子類原型導致子類的constructor屬性被次改
    p.constructor = subClass
    //設置子類原型
    subClass.prototype = p
}

組合式繼承中,通過構造函數繼承的屬性和方法是沒有問題的,所以這裏我們主要探究通過寄生式繼承重新繼承父類的原型。我們需要繼承的僅僅是父類的原型,不再需要調用父類的構造函數,換句話說,在構造函數繼承中我們已經調用了父類的構造函數。因此我們需要的就是父類的原型對象的一個副本,而這個副本我們通過原型繼承便可得到,但是這麼直接賦值給子類會有問題的,因爲對父類原型對象複製得到的複製對象p中的constructor 指向的不是subClass子類對象,因此在寄生式繼承中要對複製對象p做次增強,修復其constructor屬性指向不正確的問題,最後將得到的複製對象p賦值給子類的原型,這樣子類的原型就繼承了父類的原型並且沒有執行父類的構造函數。

//定義父類
let supClass = function(name){
    this.name = name
    this.colors = ['red', 'green', 'blue']
}
//定義父類原型方法
supClass.prototype.getName = function(){
    console.log(this.name)
}
//定義子類
let subClass = function(name, time){
    //構造函數式繼承
    supClass.call(this, name)
    //子類新增屬性
    this.time = time
}
//寄生式繼承父類原型
inheritProject(subClass, superClass)
//子類新增原型方法
subClass.prototype.getTime = function(){
    console.log(this.time) 
}
//創建兩個測試方法
let instance1 = new subClass('js book', 2014)
let instance2 = new subClass('css book', 2013)
instance1.colors.push('black')
console.log(instance1.colors)    //['red', 'green', 'blue', 'black']
console.log(instance2.colors)    //['red', 'green', 'blue']
instance2.getName()   //css book
instance2.getTime()   //2013

這種繼承方式如下圖所示:

其中最大的改變就是對子類原型的處理,被賦予父類原型的一個引用,這是一個對象,這裏要注意一點,就是子類再想添加原型方法必須通過prototype對象,否則知己賦予對象就會覆蓋掉從父類原型繼承的對象了。

 

本內容來源總結於《JavaScript設計模式》一書

原文出自於本人個人博客網站:https://www.dzyong.com(歡迎訪問)

轉載請註明來源: 鄧佔勇的個人博客 - 《JavaScript設計模式(2)—— 多種繼承方式的實現及原理

本文鏈接地址: https://www.dzyong.com/#/ViewArticle/87

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