【JS基礎系列】5種繼承方案

今天是系列第四篇,主要講一下繼承相關的問題。我發現上週原型鏈部分還有幾個概念沒有說清楚,爲了避免影響繼承知識點的學習,我決定先把上週原型鏈中的prototype、constructor和__proto__這幾個概念再作一下補充,也當作是前期回顧吧。es6

prototype是什麼?

prototype對象用於存放同一類型實例的共享屬性和方法,目的是爲了減小內存消耗。舉個生活中的例子來理解這個概念:咱們每個家庭都有購物和治病的需求,可是不可能每一個家庭都建造一個超市和醫院,這樣會形成很大的資源浪費。現代化作法是在公共區域創建一個能夠共用的超市和醫院,知足全部當地人的須要,這樣讓人們獲得了實惠,資源也被很好的使用了。以下圖:
prototype圖解app

constructor是什麼?

constructor就是一個指向自身構造函數引用的屬性。通常存在對象.constructor === 構造函數,這個概念在接下來的繼承中會有涉及。而且constructor其實是被當作共享屬性放在它的原型對象中。因此咱們能夠看作prototype.constructor === 構造函數,這個對象就是當前構造函數的原型。以下圖:
constructor圖解函數

__proto__是什麼?

我以前看到過一個結論,即:構造函數.prototype.constructor === 實例對象.__proto__.constructor === 構造函數。
咱們已經知道了原型和構造函數之間的關係,如今能夠看做有了實例對象以後怎麼跟原型產生一種關係來實現與構造函數之間的聯繫。那麼咱們可能會設置一個屬性指向原型,那麼這個屬性就是__proto__。
有了這個基礎咱們就能得出下面的結論:post

實例對象.__proto__ === 構造函數.prototype
實例對象.__proto__.constructor === 構造函數

若是運用原型鏈的知識,還有以下結論:學習

實例對象.constructor === 實例對象.__proto__.constructor

查找對象屬性時會先看當前對象是否存在該屬性,若是不存在則會去其原型鏈上找,若是原型鏈上沒有,則返回undefined。實例對象不存在constructor屬性,則會去原型鏈上找,因此和實例對象.__proto__.constructor找的是同一個值。
__proto__的基本解釋以下圖:
__proto__圖解this

有了上面的知識以後,能夠梳理繼承的知識了。如下是市面上常見的5種繼承方式:spa

  • 原型鏈繼承
  • 借用構造函數繼承
  • 組合式繼承
  • 寄生組合式繼承
  • es6繼承

原型鏈繼承

  • 優勢:可以繼承父類的原型方法
  • 缺點:prototype

    • 不能給超類型的父類傳參(即便傳參了,全部的實例獲得的屬性是同一個,例子見demo1)
    • 超類型的父類屬性是引用類型時,該屬性會被全部實例共享(由於繼承父類的全部屬性都是共享屬性,全部實例訪問到的這個屬性都是同一個內存空間,例子見demo2)
// demo1
function Parent(name){
    this.name = name
}
function Son(){}
Son.prototype = new Parent('涼涼')
Son.prototype.constructor=Son
const son1 = new Son()
const son2 = new Son()
console.log(son1.name, son2.name) // 涼涼 涼涼


// demo2
function Parent(){
    this.animals = ['老虎', '獅子']
}
function Son(){}
Son.prototype = new Parent()
Son.prototype.constructor=Son
const son1 = new Son()
const son2 = new Son()
son1.animals.push('猴子')
son2.animals.push('豬')
console.log(son1.animals) // ["老虎", "獅子", "猴子", "豬"]

本質是:將父類的實例賦值給子類的原型,讓子類的原型擁有父類的全部屬性和原型。可是原型上的全部屬性都是共享的,因此任何一個子類實例修改了原型中的屬性,其餘實例獲取到的屬性值也會引發變化。
另外還注意一點:上面的例子Son.prototype.constructor在默認狀況下是指向函數Parent,因此須要從新設置一下指向Son.prototype.constructor=Soncode

借用構造函數繼承

  • 優勢:解決父類屬性是引用類型被全部實例共享的問題和給子類傳參的問題
  • 缺點:不能繼承父類超類型的原型方法

看例子:對象

function Parent(name){
    this.name = name
    this.animals = ['老虎', '獅子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}
const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('豬')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "獅子", "猴子"]
console.log(son1.getName()) // throw error

本質是執行了一遍父類的構造函數,並讓父類構造函數的this指向子類構造函數的this(即this指向子類的實例),因此子類的實例擁有和父類實例同名屬性,可是沒有繼承原型對象。

組合式繼承

  • 優勢:集合了原型繼承和借用構造函數繼承的優勢
  • 缺點:父類構造函數會執行兩遍。子類的原型對象和原型鏈中會出現兩個相同的同名屬性

看例子:

function Parent(name){
    this.name = name
    this.animals = ['老虎', '獅子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}
Son.prototype = new Parent()
const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('豬')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "獅子", "猴子"]
console.log(son1.getName()) // 小李

從結果的輸出來看挺完美。可是在繼承的過程當中Parent函數執行了兩次,而且子類的原型對象和原型鏈中會出現兩個相同的同名屬性。由於原型鏈繼承和借用構造函數繼承都分別執行了一次。
說到這裏我有一個疑問:爲何組合繼承可以把上面兩個繼承的優勢都發揮出來呢?
答:借用構造函數的方式會在子類的實例對象上建立父類的同名屬性,原型鏈繼承的方式會在子類的原型上擁有父類的屬性和原型。可是在訪問某個對象的屬性時,會先在當前對象中找有沒有該屬性,若是不存在就會去它的原型上找。因此會先去找經過call/apply綁定在當前對象上的屬性,而不是原型中的共享屬性。因此能夠獲取到子類實例的屬性和原型。

爲了解決父類構造函數執行兩次的問題,又推出了寄生組合繼承方法。

寄生組合式繼承

  • 優勢:下降調用父類構造函數的開銷,只調用父類構造函數一次(原理是:將原型鏈繼承的那部分進行改造)
  • 缺點: /

看例子:

function Parent(name){
    this.name = name
    this.animals = ['老虎', '獅子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}

Son.prototype = Object.create(Parent.prototype)
Son.prototype.constructor = Son

const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('豬')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "獅子", "猴子"]
console.log(son1.getName()) // 小李

實質是:經過Object.create(obj)建立一個原型是obj的空對象賦值給子類的原型。還有一點須要注意:全部基於原型鏈繼承的都須要記住constructor的指向問題,寄生繼承至關因而原型鏈繼承的一種變形。

基於原型鏈的繼承若是constructor沒有從新設置指向的話,它指向的是超類型構造函數。由於constructor是原型的一個共享屬性,因此在子類原型中查找constructor屬性時其實會在原型鏈上去找constructor指向的值,最後指向了超類型構造函數。看例子:

function Parent(){}
function Son(){}
Son.prototype = new Parent()
console.log(Son.prototype.constructor) // Parent

es6繼承

最後一個是經過class,extends關鍵字實現繼承的方式。es6繼承具體函數和關鍵字的做用是什麼,下一篇文章會單獨拎出來說,先看一個class繼承的例子:

class Parent{
    constructor(name){
        this.name = name
    }
    getName(){
        return this.name
    }
}

class Son extends Parent{
    constructor(name, age){
        super(name)
        this.age = age
        console.log(new.target)
    }
    introduce(){
        return `我叫作${this.name},今年${this.age}歲了`
    }
}
const s = new Son("小李", 8)
console.log(s.introduce()) // 我叫作小李,今年8歲了
console.log(s.getName()) // 小李

總結

  • 原型繼承

    • 優勢:可以繼承父類的原型方法
    • 缺點:

      • 不能給超類型的父類傳參
      • 超類型的父類屬性是引用類型時,該屬性會被全部實例共享
  • 借用構造函數的繼承

    • 優勢:解決父類屬性是引用類型被全部實例共享的問題和給子類傳參的問題
    • 缺點:不能繼承父類超類型的原型方法
  • 組合繼承

    • 優勢:集合了原型繼承和借用構造函數繼承的優勢
    • 缺點:父類構造函數會執行兩遍。子類的原型對象和原型鏈中會出現兩個相同的同名屬性
  • 寄生組合式繼承

    • 優勢:下降調用父類構造函數的開銷,只調用父類構造函數一次(原理是:將原型鏈繼承的那部分進行改造)
    • 缺點: /
  • es6繼承

    • 優勢:引入了類的概念,使用較多語法糖,讓繼承書寫更賤簡單靈活
    • 缺點: /

參考文章

相關文章
相關標籤/搜索