今天是系列第四篇,主要講一下繼承相關的問題。我發現上週原型鏈部分還有幾個概念沒有說清楚,爲了避免影響繼承知識點的學習,我決定先把上週原型鏈中的prototype、constructor和__proto__這幾個概念再作一下補充,也當作是前期回顧吧。es6
prototype對象用於存放同一類型實例的共享屬性和方法,目的是爲了減小內存消耗。舉個生活中的例子來理解這個概念:咱們每個家庭都有購物和治病的需求,可是不可能每一個家庭都建造一個超市和醫院,這樣會形成很大的資源浪費。現代化作法是在公共區域創建一個能夠共用的超市和醫院,知足全部當地人的須要,這樣讓人們獲得了實惠,資源也被很好的使用了。以下圖:
app
constructor就是一個指向自身構造函數引用的屬性。通常存在對象.constructor === 構造函數,這個概念在接下來的繼承中會有涉及。而且constructor其實是被當作共享屬性放在它的原型對象中。因此咱們能夠看作prototype.constructor === 構造函數,這個對象就是當前構造函數的原型。以下圖:
函數
我以前看到過一個結論,即:構造函數.prototype.constructor === 實例對象.__proto__.constructor === 構造函數。
咱們已經知道了原型和構造函數之間的關係,如今能夠看做有了實例對象以後怎麼跟原型產生一種關係來實現與構造函數之間的聯繫。那麼咱們可能會設置一個屬性指向原型,那麼這個屬性就是__proto__。
有了這個基礎咱們就能得出下面的結論:post
實例對象.__proto__ === 構造函數.prototype 實例對象.__proto__.constructor === 構造函數
若是運用原型鏈的知識,還有以下結論:學習
實例對象.constructor === 實例對象.__proto__.constructor
查找對象屬性時會先看當前對象是否存在該屬性,若是不存在則會去其原型鏈上找,若是原型鏈上沒有,則返回undefined。實例對象不存在constructor屬性,則會去原型鏈上找,因此和實例對象.__proto__.constructor找的是同一個值。
__proto__的基本解釋以下圖:
this
有了上面的知識以後,能夠梳理繼承的知識了。如下是市面上常見的5種繼承方式:spa
缺點:prototype
// 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
最後一個是經過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繼承