💦【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)

JavaScript對象封裝、多態、繼承

前言

你盼世界,我盼望你無bug。Hello 你們好!我是霖呆呆!javascript

怎麼樣?小夥伴們,上一章《封裝篇(牛刀小試)》裏的十幾道題是否是作着不過癮啊。html

心裏活動:就這點水平的東西?還號稱魔鬼題前端

能夠,小夥子(姑娘),很膨脹,我喜歡。哈哈哈哈。java

既然這樣的話,就來看看這系列的大頭——繼承es6

這篇文章的繼承題但是有點東西的啊,基本覆蓋了全部主流的繼承狀況,並且都比較細節,若是你原來只是淺淺的看了一些教材,跟着手寫實現了一下而已的話,那你看完保證是會有收穫的!那樣的話還請給個三連哦 😊。面試

☑️點贊➕收藏➕關注編程

❌ 閃現➕大招➕引燃segmentfault

老規矩,不然在評論區給我一個臭臭的👎。全文共有1.7w字,前先後後整理了快兩個星期(整理真的很容易掉頭髮😂)。數組

因此還請你找個安靜的地方,在一個合適的時間來細細品味它 😊。bash

OK👌,廢話很少說,咱走着,卡加(韓語)~

JS繼承系列介紹

經過閱讀本篇文章你能夠學習到:

  • 封裝
    1. ES6以前的封裝-構造函數
    2. ES6以後的封裝-class
  • 繼承(本篇)
    1. 原型鏈繼承
    2. 構造繼承
    3. 組合繼承
    4. 寄生組合繼承
    5. 原型式繼承
    6. 寄生繼承
    7. 混入式繼承
    8. class中的extends繼承
  • 多態

(在正式閱讀本篇文章以前還請先查看封裝篇,也就是目錄的第一章節,以後觀看溫馨感更高哦 😁)

繼承

好滴👌,仍是讓咱們先來了解一下繼承的概念哈。

繼承 🤔️?

"嗯...我爸在深圳福田有一套房,之後要繼承給我"

"啪!"

"我提莫的在想什麼?我還有個弟弟,因此我爸得有兩套"

"啪!"

"你提莫還在睡,該搬磚了!"

正經點的,其實一句話來講:

繼承就是子類可使用父類的全部功能,而且對這些功能進行擴展。

好比我有個構造函數A,而後又有個構造函數B,可是B想要使用A裏的一些屬性和方法,一種辦法就是讓咱們自身化身爲CV俠,複製粘貼一波。還有一種就是利用繼承,我讓B直接繼承了A裏的功能,這樣我就能用它了。

今天要介紹的八種繼承方式在目錄中都已經列舉出來了。

不着急,從淺到深咱一個個來看。

1. 原型鏈繼承

將子類的原型對象指向父類的實例

1.1 題目一

(理解原型鏈繼承的概念)

function Parent () {
  this.name = 'Parent'
  this.sex = 'boy'
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child () {
  this.name = 'child'
}
Child.prototype = new Parent()

var child1 = new Child()
child1.getName()
console.log(child1)
複製代碼

好了,快告訴我答案吧,會打印出什麼 🤔️ ?

'child'
Child {name: "child"}
複製代碼

這...這很好理解呀

  • child1是經過子類構造函數Child生成的對象,那我就有屬性name,而且屬性值也是本身的child
  • 而後子類構造函數Child它的原型被指向了父類構造函數Parent建立出來的"無名實例"
  • 這樣的話,我child1就可使用你這個"無名實例"裏的全部屬性和方法了呀,所以child1.getName()有效。而且打印出child
  • 另外因爲sex、getName都是Child原型對象上的屬性,因此並不會表如今child1上。

這看着不就是以前都講到過的內容嘛?

就像是題目1.61.7同樣(《封裝篇(牛刀小試)》裏的)。

因此如今你知道了吧,這種方式就叫作原型鏈繼承

將子類的原型對象指向父類的實例。

咱們來寫個僞代碼,方便記憶:

Child.prototype = new Parent()
複製代碼

固然,更加嚴謹一點的作法其實還有一步:Child.prototype.constructor = Child,不過這邊霖呆呆先賣個關子,到題目4.2中咱們再來詳細說它。

1.2 題目二

不知道大家在看到原型鏈繼承這個詞語的時候,第一時間想到的是什麼?

有沒有和我同樣,想到的是把子類的原型對象指向父類的原型對象的😂:

Child.prototype = Parent.prototype
複製代碼

和我同樣的舉個手給我看下🙋‍♂️,😂

以後我就爲我xx似的想法感到慚愧...

若是我只能拿到父類原型鏈上的屬性和方法那也太廢了吧,我可不止這樣,我還想拿到父類構造函數上的屬性。

因此這道題:

function Parent () {
  this.name = 'Parent'
  this.sex = 'boy'
}
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
function Child () {
  this.name = 'child'
}
Child.prototype = Parent.prototype

var child1 = new Child()
child1.getSex()
console.log(child1)
複製代碼

結果爲:

undefined
Child {name: "child"}
複製代碼

你能夠結合上面👆的那張圖,自個兒腦補一下,child1它的原型鏈如今長啥樣了。

解析:

  • child1上能使用的屬性和方法只有name、getSex,因此getSex打印出的會是undefined
  • 打印出的child1只有name屬性,getSex爲原型上的方法因此並不會表現出來。

這道題是個錯誤的作法啊 😂

我只是爲了說明一下,爲何原型鏈繼承是要用Child.prototype = new Parent()這種方式。

1.3 題目三

(理解原型鏈繼承的優勢和缺點)

這道題的結果你們能想到嗎?

請注意對象是地址引用的哦。

function Parent (name) {
  this.name = name
  this.sex = 'boy'
  this.colors = ['white', 'black']
}
function Child () {
  this.feature = ['cute']
}
var parent = new Parent('parent')
Child.prototype = parent

var child1 = new Child('child1')
child1.sex = 'girl'
child1.colors.push('yellow')
child1.feature.push('sunshine')

var child2 = new Child('child2')

console.log(child1)
console.log(child2)

console.log(child1.name)
console.log(child2.colors)

console.log(parent)
複製代碼

答案:

Child{ feature: ['cute', 'sunshine'], sex: 'girl' }
Child{ feature: ['cute'] }

'parent'
['white', 'black', 'yellow']

Parent {name: "parent", sex: 'boy', colors: ['white', 'black', 'yellow'] }
複製代碼

解析:

  • child1在建立完以後,就設置了sex,而且給colorsfeaturepush了新的內容。
  • child1.sex = 'girl'這段代碼至關因而給child1這個實例對象新增了一個sex屬性。至關因而:本來我是沒有sex這個屬性的,我想要獲取就得拿原型對象parent上的sex,可是如今你加了一句child1.sex就等因而我本身也有了這個屬性了,就不須要你原型上的了,因此並不會影響到原型對象parent上😊。
  • 可是child1.colors這裏,注意它的操做,它是直接使用了.push()的,也就是說我得先找到colors這個屬性,發現實例對象parent上有,而後就拿來用了,以後執行push操做,因此這時候改變的是原型對象parent上的屬性,會影響到後續全部的實例對象。(這裏你會有疑問了,憑什麼sex就是在實例對象child上新增,而我colors不行,那是由於操做的方式不一樣,sex那裏是我無論你有沒有,反正我就直接用=來覆蓋你了,但是push它的前提是我得先有colors且類型是數組才行,否則你換成沒有的屬性,好比一個名爲clothes的屬性,child1.clothes.push('jacket')它直接就報錯了,若是你使用的是child1.colors = ['yellow']這樣纔不會影響parent)
  • feature它是屬於child1實例自身的屬性,它添加仍是減小都不會影響到其餘實例。
  • 所以child1打印出了featuresex兩個屬性。(namecolors屬於原型對象上的屬性並不會被表現出來)
  • child2沒有作任何操做,因此它打印出的仍是它自身的一個feature屬性😁。
  • child1.name是原型對象parent上的name,也就是'parent',雖然咱們在new Child的時候傳遞了'child1',但它顯然是無效的,由於接收name屬性的是構造函數Parent,而不是Child
  • child2.colors因爲用的也是原型對象parent上的colors,又因爲以前被child1給改變了,因此打印出來的會是['white', 'black', 'yellow']
  • 將最後的原型對象parent打印出來,namesex沒變,colors卻變了。

分析的真漂亮,漂亮的這麼一大串我都不想看了...

咳咳,不過你要是能靜下來認真的讀一讀的話就會以爲真沒啥東西,甚至不須要記什麼,我就理解了。

總結-原型鏈繼承

如今咱們就能夠得出原型鏈繼承它的優勢和缺點了

優勢:

  • 繼承了父類的模板,又繼承了父類的原型對象

缺點:

  • 若是要給子類的原型上新增屬性和方法,就必須放在Child.prototype = new Parent()這樣的語句後面
  • 沒法實現多繼承(由於已經指定了原型對象了)
  • 來自原型對象的全部屬性都被共享了,這樣若是不當心修改了原型對象中的引用類型屬性,那麼全部子類建立的實例對象都會受到影響(這點從修改child1.colors能夠看出來)
  • 建立子類時,沒法向父類構造函數傳參數(這點從child1.name能夠看出來)

這...這看到沒,壓根就不須要記,想一想霖呆呆出的這道變態的題面試的時候被問到脫口就來了。

2. instanceof

2.1 題目一

這道題主要是想介紹一個重要的運算符: instanceof

先看看官方的簡介:

instanceof 運算符用於檢測構造函數的 prototype 屬性是否出如今某個實例對象的原型鏈上。

再來看看通俗點的簡介:

a instanceof B

實例對象a instanceof 構造函數B

檢測a的原型鏈(__proto__)上是否有B.prototype,有則返回true,不然返回false

上題吧:

function Parent () {
  this.name = 'parent'
}
function Child () {
  this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
console.log(child1 instanceof Object)
複製代碼

結果爲:

true
true
true
複製代碼

這裏就利用了前面👆提到的原型鏈繼承,並且三個構造函數的原型對象都存在於child1的原型鏈上。

也就是說,左邊的child1它會向它的原型鏈中不停的查找,看有沒有右邊那個構造函數的原型對象。

例如child1 instanceof Child的查找順序:

child1 -> child1.__proto__ -> Child.prototype
複製代碼

child1 instanceof Parent的查找順序:

child1 -> child1.__proto__ -> Child.prototype
-> Child.prototype.__proto__ -> Parent.prototype
複製代碼

還不理解?

不要緊,我還有大招:

我在上面👆原型鏈繼承的思惟導圖上加了三個查找路線。

被⭕️標記的一、二、3分別表明的是Child、Parent、Object的原型對象。

好滴,一張圖簡潔明瞭。之後再碰到instanceof這種東西,按照我圖上的查找路線來查找就能夠了 😁 ~

(若是你能看到這裏,你就會發現霖呆呆的美術功底,不是通常的強)

[表情包害羞~]

2.2 題目二

(瞭解isPrototypeOf()的使用)

既然說到了instanceof,那麼就不得不提一下isPrototypeOf這個方法了。

它屬於Object.prototype上的方法,這點你能夠將Object.prototype打印在控制檯中看看。

isPrototypeOf()的用法和instanceof相反。

它是用來判斷指定對象object1是否存在於另外一個對象object2的原型鏈中,是則返回true,不然返回false

例如仍是上面👆這道題,咱們將要打印的內容改一下:

function Parent () {
  this.name = 'parent'
}
function Child () {
  this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()

console.log(Child.prototype.isPrototypeOf(child1))
console.log(Parent.prototype.isPrototypeOf(child1))
console.log(Object.prototype.isPrototypeOf(child1))
複製代碼

這裏輸出的依然是三個true

true
true
true
複製代碼

判斷的方式只要把原型鏈繼承instanceof查找思惟導圖這張圖反過來查找便可。

3. 構造繼承

瞭解了最簡單的原型鏈繼承,再讓咱們來看看構造繼承呀,也叫作構造函數繼承

在子類構造函數內部使用call或apply來調用父類構造函數

爲了方便你查看,咱們先來複習一波.callapply方法。

  • 經過call()、apply()或者bind()方法直接指定this的綁定對象, 如foo.call(obj)

  • 使用.call()或者.apply()的函數是會直接執行的

  • bind()是建立一個新的函數,須要手動調用纔會執行

  • .call().apply()用法基本相似,不過call接收若干個參數,而apply接收的是一個數組

3.1 題目一

(構造繼承的基本原理)

因此來看看這道題?

function Parent (name) {
  this.name = name
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'child')
}
var child1 = new Child()
console.log(child1)
複製代碼

child1中會有哪些屬性呢?

首先sex咱們知道確定會有的,畢竟它就是構造函數Child裏的。

其次,咱們使用了Parent.call(this, 'child').call函數剛剛已經說過了,它是會當即執行的,而這裏又用了.call來改變Parent構造函數內的指向,因此咱們是否是能夠將它轉化爲僞代碼:

function Child () {
	this.sex = 'boy'
	// 僞代碼
	this.name = 'child'
}
複製代碼

你就理解爲至關因而直接執行了Parent裏的代碼。使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類。

因此構造繼承的原理就是:

在子類構造函數內部使用call或apply來調用父類構造函數

一樣的,來寫下僞代碼:

function Child () {
    Parent.call(this, ...arguments)
}
複製代碼

arguments表示的是你能夠往裏面傳遞參數,固然這只是僞代碼)

3.2 題目二

若是你以爲上面👆這道題還不具備說明性,咱們來看看這裏。

如今我在子類和父類中都加上name這個屬性,你以爲生出來的會是好孩子仍是壞孩子呢?

function Parent (name) {
  this.name = name
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'good boy')
  this.name = 'bad boy'
}
var child1 = new Child()
console.log(child1)
複製代碼

實際上是好是壞很好區分,只要想一想3.1裏,把Parent.call(this, 'good boy')換成僞代碼就知道了。

換成了僞代碼以後,等因而重複定義了兩個相同名稱的屬性,固然是後面的覆蓋前面的啦。

因此結果爲:

Child {sex: "boy", name: "bad boy"}
複製代碼

這道題若是換一下位置:

function Child () {
  this.sex = 'boy'
  this.name = 'bad boy'
  Parent.call(this, 'good boy')
}
複製代碼

這時候就是好孩子了。

(哎,霖呆呆的產生可能就是第二種狀況...)

3.3 題目三

(構造繼承的優勢)

解決了原型鏈繼承中子類共享父類引用對象的問題

剛剛的題目都是一些基本數據類型,讓我來加上引用類型看看

function Parent (name, sex) {
  this.name = name
  this.sex = sex
  this.colors = ['white', 'black']
}
function Child (name, sex) {
  Parent.call(this, name, sex)
}
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')

var child2 = new Child('child2', 'girl')
console.log(child1)
console.log(child2)
複製代碼

這道題看着和1.3好像啊,沒錯,在父類構造函數中有一個叫colors的數組,它是地址引用的。

原型鏈繼承中咱們知道,子類構造函數建立的實例是會查找到原型鏈上的colors的,並且改動它會影響到其它的實例,這是原型鏈繼承的一大缺點。

而如今呢?你看看使用了構造繼承,結果爲:

Child{ name: 'child1', sex: 'boy', colors: ['white', 'black', 'yellow'] }
Child{ name: 'child2', sex: 'girl', colors: ['white', 'black'] }
複製代碼

咱們發現修改child1.colors並不會影響到其它的實例(child2)耶。

這裏的緣由其實咱們前面也說了:

使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類。

因此如今child1child2如今分別有它們各自的colors了,就不共享了。

並且這種拷貝屬於深拷貝,驗證的方式是你能夠把colors數組中的每一項改成一個對象,而後修改它看看。

function Parent () {
	//...
	this.colors = [{ title: 'white' }, { title: 'black' }]
}
複製代碼

所以咱們能夠得出構造繼承的優勢:

  • 解決了原型鏈繼承中子類實例共享父類引用對象的問題,實現多繼承,建立子類實例時,能夠向父類傳遞參數

3.4 題目四

(構造繼承的缺點一)

在瞭解繼承的時候,咱們老是會想到原型鏈上的屬性和方法能不能被繼承到。

採用了這種構造繼承的方式,能不能繼承父類原型鏈上的屬性呢?

來看下面👇這道題目

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'good boy')
}
Child.prototype.getSex = function () {
  console.log(this.sex)
}
var child1 = new Child()
console.log(child1)
child1.getSex()
child1.getName()
複製代碼

我給子類和父類的原型對象上都分別加了一個方法,而後調用它們。

結果居然是:

Child {sex: "boy", name: "good boy"}
'boy'
Uncaught TypeError: child1.getName is not a function 複製代碼
  • sex、name屬性都有這個咱們均可以理解
  • getSex屬於Child構造函數原型對象上的方法,咱們確定是能用它的,這個也好理解
  • getName呢?它屬於父類構造函數原型對象上的方法,報錯了?怎麼滴?我子類不配使用你啊?

你確實是不配使用我。

你使用Parent.call(this, 'good boy')只不過是讓你複製了一下我構造函數裏的屬性和方法,可沒說能讓你複製我原型對象的啊~年輕人,不要這麼貪嘛。

因此咱們能夠看出構造繼承一個最大的缺點,那就是:

小氣!

"啪!"

"你給我正經點"😂

實際上是:

  • 構造繼承只能繼承父類的實例屬性和方法,不能繼承父類原型的屬性和方法

"那不就是小氣嘛..."

"..."

3.5 題目五

(構造繼承的缺點二)

它的第二個缺點是:實例並非父類的實例,只是子類的實例。

停一下,讓咱們先來思考一下這句話的意思,而後想一想怎樣來驗證它呢 🤔️ ?

一分鐘...二分鐘...三分鐘...

啊,我知道了,剛剛不是才學的一個叫instanceof的運算符嗎?它就能檢測某個實例的原型鏈上能不能找到構造函數的原型對象。

換句話說就能檢測某個對象是否是某個構造函數的實例啦。

因此讓咱們來看看:

function Parent (name) {
  this.name = name
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'child')
}
var child1 = new Child()

console.log(child1)
console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
console.log(child1 instanceof Object)
複製代碼

結果爲:

Child {sex: "boy", name: "child"}
true
false
true
複製代碼
  • 第一個true很好理解啦,我就是你生的,你不truetrue
  • 第二個爲false其實也很好理解啦,想一想剛剛的5.3,我連你父類原型上的方法都不能用,那我和你可能也沒有關係啦,我只不過是複製了你函數裏的屬性和方法而已。
  • 第三個true,必然的,實例的原型鏈若是沒有發生改變的話最後都能找到Object.prototype啦。

(雖然說構造繼承出來的實例確實不是父類的實例,只是子類的實例。但我實際上是不太明白教材中爲何要說它是一個缺點呢?鄙人愚昧,想的多是:子類生成的實例既然能用到父類中的屬性和方法,那我就應該也要肯定這些屬性和方法的來源,若是不能使用instanceof檢測到你和父類有關係的話,那就會對這些憑空產生的屬性和方法有所質疑...)

所以構造繼承第二個缺點是:

  • 實例並非父類的實例,只是子類的實例

總結-構造繼承

構造繼承總結來講:

優勢:

  • 解決了原型鏈繼承中子類實例共享父類引用對象的問題,實現多繼承,建立子類實例時,能夠向父類傳遞參數(見題目3.3)

缺點:

  • 構造繼承只能繼承父類的實例屬性和方法,不能繼承父類原型的屬性和方法(見題目3.4)
  • 實例並非父類的實例,只是子類的實例(見題目3.5)
  • 沒法實現函數複用,每一個子類都有父類實例函數的副本,影響性能

(最後一個缺點‘沒法實現函數複用’通過評論區小夥伴matteokjh的提醒,我理解的大概是這個意思:父類構造函數中的某個函數可能只是一個功能型的函數,它不論被複制了多少份,輸出的結果或者功能都是同樣的,那麼這類函數是徹底能夠拿來複用的。可是如今用了構造函數繼承,因爲它是複製了父類構造函數中的屬性和方法,這樣產生的每一個子類實例中都會有一份本身各自的方法,但是有的方法徹底沒有必要複製,能夠用來共用的,因此就說不可以「函數複用」。)

4. 組合繼承

既然原型鏈繼承構造繼承都有這麼多的缺點,那咱們爲什麼不陰陽結合,把它們組合在一塊兒呢?

咦~

好像是個好想法。

把咱們前面的僞代碼拿來用用,想一想該如何組合呢?

// 原型鏈繼承
Child.prototype = new Parent()
// 構造繼承
function Child () {
  Parent.call(this, ...arguments)
}
複製代碼

...思考中🤔...

看到這兩段僞代碼,我好像有所頓悟了,不就是按照僞代碼裏寫的,把這兩種繼承組合在一塊兒嗎?

哇!這都被我猜中了,搜索一下組合繼承的概念,果真就是這樣。

組合繼承的概念:

組合繼承就是將原型鏈繼承與構造函數繼承組合在一塊兒,從而發揮二者之長的一種繼承模式。

思路:

  • 使用原型鏈繼承來保證子類能繼承到父類原型中的屬性和方法
  • 使用構造繼承來保證子類能繼承到父類的實例屬性和方法

基操:

  • 經過call/apply在子類構造函數內部調用父類構造函數
  • 將子類構造函數的原型對象指向父類構造函數建立的一個匿名實例
  • 修正子類構造函數原型對象的constructor屬性,將它指向子類構造函數

基操中的第一點就是構造繼承,第二點爲原型鏈繼承,第三點其實只是一個好的慣例,在後面的題目會細講到它。

4.1 題目一

(理解組合繼承的基本使用)

如今我決定對大家再也不仁慈,讓咱們換種想法,逆向思惟來解解題好很差。

陰笑~

既然我都已經說了這麼多關於組合繼承的東西了,那想必大家也知道該如何設計一個組合繼承了。

我如今須要大家來實現這麼一個ChildParent構造函數(代碼儘量地少),讓它們代碼的執行結果能以下:

(請先不要着急看答案哦,花上2分鐘來思考一下,弄清每一個屬性在什麼位置上,都有什麼公共屬性就好辦了)

var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1) // Child{ name: 'child1', sex: 'boy' }
console.log(parent1)// Parent{ name: 'parent1' }
child1.getName()    // 'child1'
child1.getSex()     // 'boy'
parent1.getName()   // 'parent1'
parent1.getSex()    // Uncaught TypeError: parent1.getSex is not a function
複製代碼

解題思路:

  • 首先來看看倆構造函數產生的實例(child1和parent1)上都有name這個屬性,因此name屬性確定是在父類的構造函數裏定義的啦,並且是經過傳遞參數進去的。
  • 其次,sex屬性只有實例child1纔有,代表它是子類構造函數上的定義的屬性(也就是咱們以前提到過的公有屬性)
  • 再而後child1parent1均可以調用getName方法,而且都沒有表如今實例上,因此它們多是在Parent.prototype上。
  • getSex對於child1是能夠調用的,對於father1是不可調用的,說明它是在Child.prototype上。

好的👌,每一個屬性各自在什麼位置上都已經找到了,再來看看如何實現它吧:

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.getSex = function () {
  console.log(this.sex)
}

var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1)
console.log(parent1)
child1.getName()
child1.getSex()
parent1.getName()
parent1.getSex()
複製代碼

不知道是否是和你構想的同樣呢 🤔️?

其實這是一道開放式題,若是構想的不同也是正常了,不過你得本身把本身構想的用代碼跑一邊看看是否是和需求同樣。

爲何說它比較開放呢?

就好比第一點,name屬性,它不必定就只存在於Parent裏呀,我Child裏也能夠有一個本身的name屬性,只不過題目要求代碼儘量地少,因此最好的就是存在與Parent中,而且用.call來實現構造繼承

另外,getName方法也不必定要在Parent.prototype上,它只要存在於parent1的原型鏈中就能夠了,因此也有可能在Object.prototype,腦補一下那張原型鏈的圖,是否是這樣呢?

這就是組合繼承帶來的魅力,若是你能看懂這道題,就已經掌握其精髓了 👏。

4.2 題目二

(理解constructor有什麼做用)

拿上面👆那道題和最開始咱們定義組合繼承的基操作對比,發現第三點constructor好像並無提到耶,可是也實現了咱們想要的功能,那這樣說來constructor好像並無什麼軟用呀...

你想的沒錯,就算咱們不對它進行任何的設置,它也絲絕不會影響到JS的內部屬性。

它不過是給咱們一個提示,用來標示實例對象是由哪一個構造函數建立的。

先用一張圖來看看constructor它存在的位置吧:

能夠看到,它實際就是原型對象上的一個屬性,指向的是構造函數。

因此咱們是否是能夠有這麼一層對應關係:

guaiguai.__proto__ = Cat.prototype
Cat.prototype.constructor = Cat
guaiguai.__proto__.constructor = Cat
複製代碼

(結合圖片來看,這樣的三角戀關係儼然並不複雜)

再結合題目4.1來看,你以爲如下代碼會打印出什麼呢?題目其實仍是4.1的題目,要求打印的東西不一樣而已。

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.getSex = function () {
  console.log(this.sex)
}

var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1.constructor)
console.log(parent1.constructor)
複製代碼

一時不知道答案也不要緊,我直接公佈一下了:

f Parent () {}
f Parent () {}
複製代碼

打印出的兩個都是Parent函數。

parent1.constructorParent函數這個還好理解,結合上面👆的圖片來看,只要經過原型鏈查找,我parent1實例自身沒有constructor屬性,那我就拿原型上的constructor,發現它指向的是構造函數Parent,所以第二個打印出Parent函數。

而對於child1,想一想組合繼承用到了原型鏈繼承,雖然也用到了構造繼承,可是構造繼承對原型鏈之間的關係沒有影響。那麼我組合繼承的原型鏈關係是否是就能夠用原型鏈繼承那張關係圖來看?

以下:

就像上面看到的同樣,原型鏈繼承切斷了本來ChildChild原型對象的關係,而是從新指向了匿名實例。使得實例child1可以使用匿名實例原型鏈上的屬性和方法。

當咱們想要獲取child1.constructor,確定是向上查找,經過__proto__找它構造函數的原型對象匿名實例

可是匿名實例它自身是沒有constructor屬性的呀,它只是Parent構造函數建立出來的一個對象而已,因此它也會繼續向上查找,而後就找到了Parent原型對象上的constructor,也就是Parent了。

因此回過頭來看看這句話:

construcotr它不過是給咱們一個提示,用來標示實例對象是由哪一個構造函數建立的。

從人(常)性(理)的角度上來看,child1Child構建的,parent1Parent構建的。

那麼child1它的constructor就應該是Child呀,可是如今卻變成了Parent,貌似並不太符合常理啊。

因此纔有了這麼一句:

Child.prototype.constructor = Child
複製代碼

用以修復constructor的指向。

如今讓咱們經過改造原型鏈繼承思惟導圖來畫畫組合繼承的思惟導圖吧。

(至於爲何在組合繼承中我修復了constructor,在原型鏈繼承中沒有,這個其實取決於你本身,由於你也看到了constructor實際並無什麼做用,不過面試被問到的話確定是要知道的)

總結來講:

  • constructor它是構造函數原型對象中的一個屬性,正常狀況下它指向的是原型對象。
  • 它並不會影響任何JS內部屬性,只是用來標示一下某個實例是由哪一個構造函數產生的而已。
  • 若是咱們使用了原型鏈繼承或者組合繼承無心間修改了constructor的指向,那麼出於編程習慣,咱們最好將它修改成正確的構造函數。

4.3 題目三

constructor的某個使用場景)

先來看看下面👇這道題:

var a;
(function () {
  function A () {
    this.a = 1
    this.b = 2
  }
  A.prototype.logA = function () {
    console.log(this.a)
  }
  a = new A()
})()

a.logA()
複製代碼

這裏的輸出結果:

1
複製代碼

乍一看被整片的a給搞糊了,可是仔細分析來,就能得出結果了。

  • 定義了一個全局的變量a,和一個構造函數A
  • 在當即執行函數中,是能夠訪問到全局變量a的,所以a被賦值爲了一個構造函數A生成的對象
  • 而且a對象中有兩個屬性:ab,且值都是1
  • 以後在外層調用a.logA(),打印出的就是a.a,也就是1

難度升級:

如今我想要在匿名函數外給A這個構造函數的原型對象中添加一個方法logB用以打印出this.b

你首先想到的是否是B.prototype.logB = funciton() {}

可是注意咯,我是要你在匿名函數外添加,而此時因爲做用域的緣由,咱們在匿名函數外是訪問不到A的,因此這樣的作法就不可行了。

解決辦法:

雖然咱們在外層訪問不到A,可是咱們能夠經過原型鏈查找,來獲取A的原型對象呀。

仍是這張圖:

這裏咱們就有兩種解決辦法了:

  1. 經過a.__proto__來訪問到原型對象:
a.__proto__.logB = function () {
  console.log(this.b)
}
a.logB()
複製代碼
  1. 經過a.constructor.prototype來訪問到原型對象:
a.constructor.prototype.logB = function () {
  console.log(this.b)
}
a.logB()
複製代碼

想一想是否是這樣的?

雖然我a實例上沒有constructor,可是原型對象上有呀,因此a.construtor實際拿的是原型對象上的construtor

(我的愚見感受並沒什麼軟用...我用__proto__就能夠了呀 😂)

4.4 題目四

(理解組合繼承的優勢)

function Parent (name, colors) {
  this.name = name
  this.colors = colors
}
Parent.prototype.features = ['cute']
function Child (name, colors) {
  this.sex = 'boy'
  Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1', ['white'])
child1.colors.push('yellow')
child1.features.push('sunshine')
var child2 = new Child('child2', ['black'])

console.log(child1)
console.log(child2)
console.log(Child.prototype)

console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
複製代碼

有了前面幾題做爲基礎,這道題也就不難了。

答案:

Child{ sex: "boy", name: "child1", colors: ["white", "yellow"] }
Child{ sex: "boy", name: "child2", colors: ["black"] }
Parent{ name: undefined, colors: undefined, constructor: f Child () {} }

true
true
複製代碼

解析思路:

  • 兩個childsexname都沒啥問題,而colors可能會有些疑問,由於colors是經過構造繼承於父類的,而且是複製出來的屬性,因此改變child1.colors並不會影響child2.colors。(相似題目3.3)
  • Child.prototype,是使用new Parent生成的,而且生成的時候是沒有傳遞參數進去的,所以namecolors都是undefined。並且題目中又將constructor給修正指向了Child
  • 最後兩個true,是由於child1能夠沿着它的原型鏈查找到Child.prototypeParent.prototype。(相似題目2.1)

如今你就能夠看出組合繼承的優勢了吧,它其實就是將兩種繼承方式的優勢給結合起來。

  • 能夠繼承父類實例屬性和方法,也可以繼承父類原型屬性和方法
  • 彌補了原型鏈繼承中引用屬性共享的問題
  • 可傳參,可複用

4.5 題目五

(理解組合繼承的缺點)

人無完人,狗無完狗,就算是組合繼承這麼牛批的繼承方式也仍是有它的缺點 😁。

一塊兒來看看這裏:

function Parent (name) {
  console.log(name) // 這裏有個console.log()
  this.name = name
}
function Child (name) {
  Parent.call(this, name)
}
Child.prototype = new Parent()
var child1 = new Child('child1')

console.log(child1)
console.log(Child.prototype)
複製代碼

執行結果爲:

undefined
'child1'

Child{ name: 'child1' }
Parent{ name: undefined }
複製代碼

咱們雖然只調用了new Child()一次,可是在Parent中卻兩次打印出了name

  • 第一次是原型鏈繼承的時候,new Parent()
  • 第二次是構造繼承的時候,Parent.call()調用的

也就是說,在使用組合繼承的時候,會憑空多調用一次父類構造函數。

另外,咱們想要繼承父類構造函數裏的屬性和方法採用的是構造繼承,也就是複製一份到子類實例對象中,而此時因爲調用了new Parent(),因此Child.prototype中也會有一份如出一轍的屬性,就例如這裏的name: undefined,但是我子類實例對象本身已經有了一份了呀,因此我怎麼也用不上Child.prototype上面的了,那你這憑空多出來的屬性不就佔了內存浪費了嗎?

所以咱們能夠看出組合繼承的缺點:

  • 使用組合繼承時,父類構造函數會被調用兩次
  • 而且生成了兩個實例,子類實例中的屬性和方法會覆蓋子類原型(父類實例)上的屬性和方法,因此增長了沒必要要的內存。

4.6 題目六

(考察你是否理解實例對象上引用類型和原型對象上引用類型的區別)

這裏可就有一個坑了,得注意了⚠️:

function Parent (name, colors) {
  this.name = name
  this.colors = colors
}
Parent.prototype.features = ['cute']
function Child (name, colors) {
  Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child

var child1 = new Child('child1', ['white'])
child1.colors.push('yellow')
child1.features.push('sunshine')
var child2 = new Child('child2', ['black'])

console.log(child1.colors)
console.log(child2.colors)
console.log(child1.features)
console.log(child2.features)
複製代碼

題目解析:

  • colors屬性雖然定義在Parent構造函數中,可是Child經過構造繼承複製了其中的屬性,因此它存在於各個實例當中,改變child1裏的colors就不會影響其它地方了
  • features是定義在父類構造函數原型對象中的,是比new Parent()還要更深一層的對象,在child實例還有Child.prototype(也就是new Parent()產生出了的匿名實例)上都沒有features屬性,所以它們只能去它們共有的Parent.prototype上面拿了,因此這時候它們就是共用了一個features,所以改變child1.features就會改變child2.features了。

結果爲:

["white", "yellow"]
["black"]
["cute", "sunshine"]
["cute", "sunshine"]
複製代碼

但是霖呆呆不對呀,你剛剛不是還說了:

組合繼承彌補了原型鏈繼承中引用屬性共享的問題

就在題4.4中,都還熱乎着呢?怎麼這裏的features仍是沒有被解決啊,它們仍是共享了。

"冤枉啊!我歷來不騙人"

它確實是解決了原型鏈繼承中引用屬性共享的問題啊,你想一想這裏Child.prototype是誰?

是否是new Parent()產生的那個匿名實例?而這個匿名實例中的引用類型是否是colors?而colors是否是確實不是共享的?

那就對了呀,我已經幫你解決了原型(匿名實例)中引用屬性共享的問題了呀。

至於featuresParent.prototype上的屬性,至關因而爺爺那一級別的了,這我可無法子。

總結-組合繼承

一樣的,讓咱們對組合繼承也來作個總結吧:

實現方式:

  • 使用原型鏈繼承來保證子類能繼承到父類原型中的屬性和方法
  • 使用構造繼承來保證子類能繼承到父類的實例屬性和方法

優勢:

  • 能夠繼承父類實例屬性和方法,也可以繼承父類原型屬性和方法
  • 彌補了原型鏈繼承中引用屬性共享的問題
  • 可傳參,可複用

缺點:

  • 使用組合繼承時,父類構造函數會被調用兩次
  • 而且生成了兩個實例,子類實例中的屬性和方法會覆蓋子類原型(父類實例)上的屬性和方法,因此增長了沒必要要的內存。

constructor總結:

  • constructor它是構造函數原型對象中的一個屬性,正常狀況下它指向的是原型對象。
  • 它並不會影響任何JS內部屬性,只是用來標示一下某個實例是由哪一個構造函數產生的而已。
  • 若是咱們使用了原型鏈繼承或者組合繼承無心間修改了constructor的指向,那麼出於編程習慣,咱們最好將它修改成正確的構造函數。

5. 寄生組合繼承

唔...寄生這個詞聽着有點可怕啊...

它比組合繼承還要牛批一點。

剛剛咱們提了組合繼承的缺點無非就是:

  1. 父類構造函數會被調用兩次
  2. 生成了兩個實例,在父類實例上產生了無用廢棄的屬性

那麼有沒有一種方式讓咱們直接跳過父類實例上的屬性,而讓我直接就能繼承父類原型鏈上的屬性呢?

也就是說,咱們須要一個乾淨的實例對象,來做爲子類的原型。而且這個乾淨的實例對象還得能繼承父類原型對象裏的屬性。

咦~說到乾淨的對象,我就想到了一個方法:Object.create()

讓咱們先來回憶一波它的用法:

Object.create(proto, propertiesObject)
複製代碼
  • 參數一,須要指定的原型對象
  • 參數二,可選參數,給新對象自身添加新屬性以及描述器

在這裏咱們主要講解一下第一個參數proto,它的做用就是能指定你要新建的這個對象它的原型對象是誰。

怎麼說呢?

就比如,咱們使用var parent1 = new Parent()建立了一個對象parent1,那parent1.__proto__就是Parent.prototype

使用var obj = new Object()建立了一個對象obj,那obj.__proto__就是Object.prototype

而這個Object.create()屌了,它如今能指定你新建對象的__proto__

哈哈哈哈~

這正不是咱們想要的嗎?咱們如今只想要一個乾淨而且能連接到父類原型鏈上的對象。

來看看題目一。

5.1 題目一

(理解寄生組合繼承的用法)

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
// 與組合繼承的區別
Child.prototype = Object.create(Parent.prototype)

var child1 = new Child('child1')

console.log(child1)
child1.getName()

console.log(child1.__proto__)
console.log(Object.create(null))
console.log(new Object())
複製代碼

能夠看到,上面👆這道題就是一個標準的寄生組合繼承,它與組合繼承的區別僅僅是Child.prototype不一樣。

咱們使用了Object.create(Parent.prototype)建立了一個空的對象,而且這個對象的__proto__屬性是指向Parent.prototype的。

來看看寄生組合繼承的思惟導圖:

(靈魂畫手再次上線)

能夠看到,如今Parent()已經和child1沒有關係了,僅僅是用了Parent.call(this)來複制了一下Parent裏的屬性和方法 😁。

所以這道題的答案爲:

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
// 與組合繼承的區別
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

var child1 = new Child('child1')

console.log(child1) // Child{ sex: "boy", name: "child1" }
child1.getName() // "child1"

console.log(child1.__proto__) // Parent{}
console.log(Object.create(null)) // {}
console.log(new Object()) // {}
複製代碼

題目解析:

  • 使用寄生組合繼承child1不只僅有本身的實例屬性sex,並且還複製了父類中的屬性name
  • 寄生組合繼承使得實例child1能經過原型鏈查找,使用到Parent.prototype上的方法,所以打印出child1

最後的三個空對象,咱們就須要展開來看看了:

  • child1.__proto__也就是Child.prototype,也就是Object.create(Parent.prototype),這個空對象它的__proto__指向的就是咱們想要的父類的原型對象,因此child1就能使用Parent.prototype上的方法了。
  • 而經過Object.create(null)建立的對象呢?哇,這可真的是空的不能再空了,由於咱們建立它的時候傳遞的參數是null,也就是將它的__proto__屬性設置爲null,那它就至關因而沒有原型鏈了,連Object.prototype上的方法它都不能用了(好比toString()、hasOwnProperty())
  • 再來看看new Object(),這個其實很好理解了,Object自己就是一個構造函數,就像Parent、Child這種,只不過它的原型對象是咱們經常使用的Object.prototype

(看看,你們在學繼承的同時,還順便學習了一波Object.create(),多好啊 😁)

5.2 題目二

雖然寄生組合繼承組合繼承很是像,不過咱們仍是來看一道題鞏固鞏固吧。

執行結果:

Child{ name: 'child1', face: 'smile', sex: 'boy', colors: ['white', 'black', 'yellow'] }
Child{ name: 'child2', face: 'smile', sex: 'boy', colors: ['white', 'black'], features: ['sunshine'] }

["cute"]
["sunshine"]
複製代碼

哈哈哈,小夥伴們的答案和這裏是否有出入呢?

是否是發現一不當心就會作錯 😂。

讓咱們來看看解題思路:

  • name、face、sex三個屬性都沒有啥問題,要注意的只是face屬性,後面寫的會覆蓋前面的(相似題目3.2)
  • colors屬性是經過構造繼承複製過來的,因此改變child1.colors對其餘實例沒有影響,這個說過不少次了。
  • 要注意的就是這裏的features,在沒有執行child2.features = ['sunshine']這段代碼以前,child1child2都是共用原型鏈上的features,可是執行了這段代碼以後,就至關因而給child2對象上新增了一個名爲features屬性,因此這時候child2取的就是它自身的了。

(這道題我是使用VSCode插件Polacode-2019作的代碼截圖,不知道你們是喜歡這種代碼截圖仍是喜歡源代碼的形式呢?能夠留言告訴霖呆呆 😁)

(另外,關於更多美化工具的使用能夠查看個人這篇文章:你的掘金文章本能夠這麼炫(博客美化工具一波帶走)

總結-寄生組合繼承

寄生組合繼承算是ES6以前一種比較完美的繼承方式吧。

它避免了組合繼承中調用兩次父類構造函數,初始化兩次實例屬性的缺點。

因此它擁有了上述全部繼承方式的優勢:

  • 只調用了一次父類構造函數,只建立了一份父類屬性
  • 子類能夠用到父類原型鏈上的屬性和方法
  • 可以正常的使用instanceOfisPrototypeOf方法

6. 原型式繼承

算是翻了不少關於JS繼承的文章吧,其中百分之九十都是這樣介紹原型式繼承的:

該方法的原理是建立一個構造函數,構造函數的原型指向對象,而後調用 new 操做符建立實例,並返回這個實例,本質是一個淺拷貝。

僞代碼以下:

(後面會細講)

function objcet (obj) {
    function F () {};
    F.prototype = obj;
    F.prototype.constructor = F;
    return new F();
}
複製代碼

開始覺得是多神祕的東西,但後來真正瞭解了它以後感受用的應該很少吧... 😢

先來看看題目一。

6.1 題目一

在真正開始看原型式繼承以前,先來看個咱們比較熟悉的東西:

var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}

var guaiguai = Object.create(cat)
var huaihuai = Object.create(cat)

console.log(guaiguai)
console.log(huaihuai)

console.log(guaiguai.heart)
console.log(huaihuai.colors)
複製代碼

這裏的執行結果:

{}
{}

'❤️'
['white', 'black']
複製代碼

這裏用到了咱們以前提到過的Object.create()方法。

在這道題中,Object.create(cat)會建立出一個__proto__屬性爲cat的空對象。

因此你能夠看到乖乖壞壞都是一隻空貓,可是它們卻能用貓cat的屬性。

6.2 題目二

不怕你笑話,上面👆說的這種方式就是原型式繼承,只不過在ES5以前,尚未Object.create()方法,因此就會用開頭介紹的那段僞代碼來代替它。

將題目6.1改造一下,讓咱們本身來實現一個Object.create()

咱們就將要實現的函數命名爲create()

想一想Object.create()的做用:

  • 它接受的是一個對象
  • 返回的是一個新對象,
  • 新對象的原型鏈中必須能找到傳進來的對象

因此就有了這麼一個方法:

function objcet (obj) {
    function F () {};
    F.prototype = obj;
    F.prototype.constructor = F;
    return new F();
}
複製代碼

它知足了上述的幾個條件。

來看看效果是否是和題6.1同樣呢?

function objcet (obj) {
    function F () {};
    F.prototype = obj;
    F.prototype.constructor = F;
    return new F();
}
var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}

var guaiguai = create(cat)
var huaihuai = create(cat)

console.log(guaiguai)
console.log(huaihuai)

console.log(guaiguai.heart)
console.log(huaihuai.colors)
複製代碼

執行結果爲:

效果是和Object.create()差很少(只不過咱們自定義的create返回的對象是構造函數F建立的)。

這就有小夥伴要問了,既然是須要知足

  • 新對象的原型鏈中必須能找到傳進來的對象

這個條件的話,我這樣寫也能夠實現啊:

function create (obj) {
    var newObj = {}
    newObj.__proto__ = obj
    return newObj;
}
複製代碼

請注意了,咱們是要模擬Object.create()方法,若是你都能使用__proto__,那爲什麼不乾脆使用Object.create()呢?(它們是同一時期的產物)

總結-原型式繼承

因爲它使用的不太多,這裏就很少說它了。

(霖呆呆就是這麼現實)

不過仍是要總結一下滴:

實現方式:

該方法的原理是建立一個構造函數,構造函數的原型指向對象,而後調用 new 操做符建立實例,並返回這個實例,本質是一個淺拷貝。

ES5以後能夠直接使用Object.create()方法來實現,而在這以前就只能手動實現一個了(如題目6.2)。

優勢:

  • 再不用建立構造函數的狀況下,實現了原型鏈繼承,代碼量減小一部分。

缺點:

  • 一些引用數據操做的時候會出問題,兩個實例會公用繼承實例的引用數據類
  • 謹慎定義方法,以避免定義方法也繼承對象原型的方法重名
  • 沒法直接給父級構造函數使用參數

(呀!很久沒用表情包了,此處應該有個表情包)

7. 寄生式繼承

cccc...

怎麼又來了個什麼寄生式繼承啊,還有完沒完...

心態放平和...

其實這個寄生式繼承也沒啥東西的,它就是在原型式繼承的基礎上再封裝一層,來加強對象,以後將這個對象返回。

來看看僞代碼你就知道了:

function createAnother (original) {
    var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象
    clone.fn = function () {}; // 以某種方式來加強對象
    return clone; // 返回這個對象
}
複製代碼

7.1 題目一

(瞭解寄生式繼承的使用方式)

它的使用方式,唔...

例如我如今想要繼承某個對象上的屬性,同時又想在新建立的對象中新增上一些其它的屬性。

來看下面👇這兩隻貓咪

var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}
function createAnother (original) {
    var clone = Object.create(original);
    clone.actingCute = function () {
      console.log('我是一隻會賣萌的貓咪')
    }
    return clone;
}
var guaiguai = createAnother(cat)
var huaihuai = Object.create(cat)

guaiguai.actingCute()
console.log(guaiguai.heart)
console.log(huaihuai.colors)
console.log(guaiguai)
console.log(huaihuai)
複製代碼

題目解析:

  • guaiguai是一直通過加工的小貓咪,因此它會賣萌,所以調用actingCute()會打印賣萌
  • 兩隻貓都是經過Object.create()進行過原型式繼承cat對象的,因此是共享使用cat對象中的屬性
  • guaiguai通過createAnother新增了自身的實例方法actingCute,因此會有這個方法
  • huaihuai是一隻空貓,由於heart、colors都是原型對象cat上的屬性

執行結果:

'我是一隻會賣萌的貓咪'
'❤️'
['white', 'black']
{ actingCute: ƒ }
{}
複製代碼

總結-寄生式繼承

實現方式:

  • 原型式繼承的基礎上再封裝一層,來加強對象,以後將這個對象返回。

優勢:

  • 再不用建立構造函數的狀況下,實現了原型鏈繼承,代碼量減小一部分。

缺點:

  • 一些引用數據操做的時候會出問題,兩個實例會公用繼承實例的引用數據類
  • 謹慎定義方法,以避免定義方法也繼承對象原型的方法重名
  • 沒法直接給父級構造函數使用參數

8. 混入方式繼承多個對象

過五關斬六將,咱終於到了ES5中的要講的最後一種繼承方式了。

這個混入方式繼承其實很好玩,以前咱們一直都是以一個子類繼承一個父類,而混入方式繼承就是教咱們如何一個子類繼承多個父類的。

在這邊,咱們須要用到ES6中的方法Object.assign()

它的做用就是能夠把多個對象的屬性和方法拷貝到目標對象中,如果存在同名屬性的話,後面的會覆蓋前面。(固然,這種拷貝是一種淺拷貝啦)

來看看僞代碼:

function Child () {
    Parent.call(this)
    OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
複製代碼

8.1 題目一

(理解混入方式繼承的使用)

額,既然您都看到這了,說明實力以及很強了,要不?咱直接就上個複雜點的題?

function Parent (sex) {
  this.sex = sex
}
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
function OtherParent (colors) {
  this.colors = colors
}
OtherParent.prototype.getColors = function () {
  console.log(this.colors)
}
function Child (sex, colors) {
  Parent.call(this, sex)
  OtherParent.call(this, colors) // 新增的父類
  this.name = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父類原型對象
Child.prototype.constructor = Child

var child1 = new Child('boy', ['white'])
child1.getSex()
child1.getColors()
console.log(child1)
複製代碼

這裏就是採用了混入方式繼承,在題目中標出來的地方就是不一樣於寄生組合繼承的地方。

如今的child1不只複製了Parent上的屬性和方法,還複製了OtherParent上的。

並且它不只可使用Parent.prototype的屬性和方法,還能使用OtherParent.prototype上的。

結果:

'boy'
['white']
{ name: 'child', sex: 'boy', colors: ['white'] }
複製代碼

8.2 題目二

(理解混入方式繼承的原型鏈結構)

同是上面👆的題,我如今多加上幾個輸出:

function Parent (sex) {
  this.sex = sex
}
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
function OtherParent (colors) {
  this.colors = colors
}
OtherParent.prototype.getColors = function () {
  console.log(this.colors)
}
function Child (sex, colors) {
  Parent.call(this, sex)
  OtherParent.call(this, colors) // 新增的父類
  this.name = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父類原型對象
Child.prototype.constructor = Child

var child1 = new Child('boy', ['white'])
// child1.getSex()
// child1.getColors()
// console.log(child1)

console.log(Child.prototype.__proto__ === Parent.prototype)
console.log(Child.prototype.__proto__ === OtherParent.prototype)
console.log(child1 instanceof Parent)
console.log(child1 instanceof OtherParent)
複製代碼

這四個輸出你感受會是什麼 🤔️?

先不要着急,若是有條件的,本身動手在紙上把如今的原型鏈關係給畫一下。

反正呆呆是已經用XMind的畫好了:

能夠看到,其實它與前面咱們畫的寄生組合繼承思惟導圖就多了下面OtherParent的那部分東西。

  • Child內使用了call/apply來複制構造函數OtherParent上的屬性和方法
  • Child.prototype使用Object.assign()淺拷貝OtherParent.prototype上的屬性和方法

根據這這幅圖,咱們很快就能得出答案了:

true
false
true
false
複製代碼

9. class中的繼承

構造函數中主要的幾種繼承方式都已經介紹的差很少了,接下來就讓咱們看看ES6class的繼承吧。

class 中繼承主要是依靠兩個東西:

  • extends
  • super

並且對於該繼承的效果和以前咱們介紹過的寄生組合繼承方式同樣。(沒錯,就是那個最屌的繼承方式)

一塊兒來看看題目一 😁。

9.1 題目一

(理解class中的繼承)

既然它的繼承和寄生組合繼承方式同樣,那麼讓咱們將題目5.1的題目改造一下,用class的繼承方式來實現它。

class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
class Child extends Parent {
  constructor (name) {
    super(name)
    this.sex = 'boy'
  }
}
var child1 = new Child('child1')
console.log(child1)
child1.getName()

console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
複製代碼

結果以下:

Child{ name: 'child1', sex: 'boy' }
'child1'
true
true
複製代碼

再讓咱們來寫一下寄生組合繼承的實現方式:

function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

var child1 = new Child('child1')
console.log(child1)
child1.getName()

console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
複製代碼

結果以下:

Child{ name: 'child1', sex: 'boy' }
'child1'
true
true
複製代碼

這樣好像看不出個啥,沒事,讓咱們上圖:

class繼承

寄生組合繼承

能夠看到,class的繼承方式徹底知足於寄生組合繼承。

9.2 題目二

(理解extends的基本做用)

能夠看到上面👆那道題,咱們用到了兩個關鍵的東西:extendssuper

extends從字面上來看仍是很好理解的,對某個東西的延伸,繼承。

那若是咱們單單隻用extends不用super呢?

class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
class Child extends Parent {
  // constructor (name) {
  // super(name)
  // this.sex = 'boy'
  // }
  sex = 'boy' // 實例屬性sex放到外面來
}
var child1 = new Child('child1')
console.log(child1)
child1.getName()
複製代碼

其實這裏的執行結果和沒有隱去以前同樣。

執行結果:

那咱們是否是能夠認爲:

class Child extends Parent {}

// 等同於
class Child extends Parent {
    constructor (...args) {
        super(...args)
    }
}
複製代碼

OK👌,其實這一步很好理解啦,還記得以前咱們就提到過,在class中若是沒有定義constructor方法的話,這個方法是會被默認添加的,那麼這裏咱們沒有使用constructor,它其實已經被隱式的添加和調用了。

因此咱們能夠看出extends的做用:

  • class能夠經過extends關鍵字實現繼承父類的全部屬性和方法
  • 如果使用了extends實現繼承的子類內部沒有constructor方法,則會被默認添加constructorsuper

9.3 題目三

(理解super的基本做用)

經過上面那道題看來,constructor貌似是無關緊要的角色。

那麼super呢,它在 class中扮演的是一個什麼角色 🤔️?

仍是上面的題目,可是此次我不使用super,看看會有什麼效果:

class Parent {
  constructor () {
    this.name = 'parent'
  }
}
class Child extends Parent {
  constructor () {
    // super(name) // 把super隱去
  }
}
var child1 = new Child()
console.log(child1)
child1.getName()
複製代碼

哈哈哈,如今你保存刷新頁面,就會發現它報錯了:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    at new Child
複製代碼

你品你細細品。

大體意思就是你必須得在constructor中調用一下super函數。

這樣說來,constructorsuper是一對好基友啊...

super函數咱仍是不能省,很重要啊。

而後再看了看它的寫法,有點像是給父級類中傳遞參數的感受啊 😄。

唔...若是你這樣想的話算是猜對了一部分吧。這其實和ES6的繼承機制有關。

  • 咱們知道在ES5中的繼承(例如構造繼承、寄生組合繼承) ,實質上是先創造子類的實例對象this,而後再將父類的屬性和方法添加到this上(使用的是Parent.call(this))。
  • 而在ES6中卻不是這樣的,它實質是先創造父類的實例對象this(也就是使用super()),而後再用子類的構造函數去修改this

通俗理解就是,子類必須得在constructor中調用super方法,不然新建實例就會報錯,由於子類本身沒有本身的this對象,而是繼承父類的this對象,而後對其加工,若是不調用super的話子類就得不到this對象。

哇哦~

[果真是好基友~]

這道題介紹的是super的基本做用,下面來講說它的具體用法吧。

9.4 題目四

(super看成函數調用時)

super其實有兩種用法,一種是看成函數來調用,還有一種是當作對象來使用。

以前那道題就是將它當成函數來調用的,並且咱們知道在constructor中還必須得執行super()

其實,super被看成函數調用時,表明着父類的構造函數

雖然它表明着父類的構造函數,可是返回的倒是子類的實例,也就是說super內部的this指向的是Child

讓咱們來看道題驗證一下:

(new.target指向當前正在執行的那個函數,你能夠理解爲new後面的那個函數)

class Parent {
  constructor () {
    console.log(new.target.name)
  }
}
class Child extends Parent {
  constructor () {
    var instance = super()
    console.log(instance)
    console.log(instance === this)
  }
}
var child1 = new Child()

var parent1 = new Parent()

console.log(child1)
console.log(parent1)
複製代碼

這道題中,我在父類的constructor中打印出new.target.name

而且用了一個叫作instance的變量來盛放super()的返回值。

而剛剛咱們已經說了,super的調用表明着父類構造函數,那麼這邊我在調用new Child的時候,它裏面也執行了父類的constructor函數,因此console.log(new.target.name)確定被執行了兩遍了(一遍是new Child,一遍是new Parent)

因此這裏的執行結果爲:

'Child'
Child{}
true

'Parent'

Child{}
Parent{}
複製代碼
  • new.target表明的是new後面的那個函數,那麼new.target.name表示的是這個函數名,因此在執行new Child的時候,因爲調用了super(),因此至關於執行了Parent中的構造函數,所以打印出了'Child'
  • 另外,關於super()的返回值instance,剛剛已經說了它返回的是子類的實例,所以instance會打印出Child{};而且instance和子類construtor中的this相同,因此打印出true
  • 而執行new Parent的時候,new.target.name打印出的就是'Parent'了。
  • 最後分別將child1parent1打印出來,都沒什麼問題。

經過這道題咱們能夠看出:

  • super當成函數調用時,表明父類的構造函數,且返回的是子類的實例,也就是此時super內部的this指向子類。
  • 在子類的constructorsuper()就至關因而Parent.constructor.call(this)

9.5 題目五

(super當成函數調用時的限制)

剛剛已經說明了super當成函數調用的時候就至關因而用call來改變了父類構造函數中的this指向,那麼它的使用有什麼限制呢?

  • 子類constructor中若是要使用this的話就必須放到super()以後
  • super當成函數調用時只能在子類的construtor中使用

來看看這裏:

class Parent {
  constructor (name) {
    this.name = name
  }
}
class Child extends Parent {
  constructor (name) {
    this.sex = 'boy'
    super(name)
  }
}
var child1 = new Child('child1')
console.log(child1)
複製代碼

你以爲這裏會打印出什麼呢 🤔️?

其實這裏啥都不會打印,控制檯是紅色的。

報了個和7.3同樣的錯:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    at new Child
複製代碼

這也就符合了剛剛說到的第一點:子類constructor中若是要使用this的話就必須放到super()以後。

這點其實很是好理解,還記得super的做用嗎?在constructor中必須得有super(),它就是用來產生實例this的,那麼再調用它以前,確定是訪問不到this的啦。

也就是在this.sex = 'boy'這一步的時候就已經報錯了。

至於第二點,super被當成函數來調用的話就必須得放到constructor中,在其它的地方使用它就是咱們接下來要說的super當成對象使用的狀況。

9.6 題目六

(super當成對象來使用時)

super若是當成一個對象來調用的話,唔...那也可能存在於class裏的不一樣地方呀。

好比constructor、子類實例方法、子類構造方法,在這些地方它分別指代的是什麼呢?

咱們只須要記住:

  • 在子類的普通函數中super對象指向父類的原型對象
  • 在子類的靜態方法中super對象指向父類

依靠着這個準則,咱們來作作下面👇這道題:

class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
Parent.prototype.getSex = function () {
	console.log('boy')
}
Parent.getColors = function () {
  console.log(['white'])
}
class Child extends Parent {
  constructor (name) {
    super(name)
    super.getName()
  }
  instanceFn () {
    super.getSex()
  }
  static staticFn () {
    super.getColors()
  }
}
var child1 = new Child('child1')
child1.instanceFn()
Child.staticFn()
console.log(child1)
複製代碼

經過學習《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》咱們知道各個方法所在的位置:

  • getName爲父類原型對象上的方法
  • getSex爲父類原型對象上的方法
  • getColors爲父類的靜態方法
  • instanceFn爲子類原型對象上方法
  • staticFn爲子類的靜態方法

題目分析:

  • 在使用new Child('child1')建立child1的時候,會執行子類constructor中的方法,所以會執行super.getName(),而依靠準則一,此時的constructor中的第二個super指向的是父類的原型對象,所以此時super.getName()會被成功調用,並打印出'child1'。(第一個super是當成函數來調用)
  • child1建立完以後,執行了child1.instanceFn(),這時候依據準則一,instanceFn函數中的super指向的仍是父類的原型對象,所以super.getSex()也會被成功調用,並打印出'boy'
  • staticFn屬於子類的靜態方法,因此須要使用Child.staticFn()來調用,且依據準則二,此時staticFn中的super指向的是父類,也就是Parent這個類,所以調用其靜態方法getColors成立,打印出['white']
  • 最後須要打印出child1,咱們只須要知道哪些是child1的實例屬性和方法就能夠了,經過比較很容易就發現,child1中就只有一個name屬性是經過調用super(name)從父級那裏複製來的,其它方法都不能被child1"表現"出來,可是能夠調用。

因此執行結果爲:

'child1'
'boy'
['white']
Child{ name: 'child1' }
複製代碼

"Good for you! 我貌似已經掌握它嘞"

9.7 題目七

(super當成對象調用父類方法時this的指向)

在作剛剛那道題的時候,額,大家就對super.getName()的打印結果沒啥疑問嗎 🤔️?

(難道是我吹的太有模有樣讓你忽略了它?)

既然super.getName()getName是被super調用的,而我卻說此時的super指向的是父類原型對象。那麼getName內打印出的應該是父類原型對象上的name,也就是undefined呀,怎麼會打印出child1呢?

帶着這個疑問我寫下了這道題:

class Parent {
  constructor () {}
}
Parent.prototype.sex  = 'boy'
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
class Child extends Parent {
  constructor () {
    super()
    this.sex = 'girl'
    super.getSex()
  }
}
var child1 = new Child()
console.log(child1)
複製代碼

如今父類原型對象和子類實例對象child1上都有sex屬性,且不相同。

若是按照this指向來看,調用super.getSex()打印出的應該是Parent.prototype上的sex'boy'

就像是這樣調用同樣:Parent.prototype.getSex()

可是結果倒是:

'girl'
Child{ sex: 'girl' }
複製代碼

唔...其實扯了這麼一大堆,我只是想告訴你:

  • ES6規定,經過super調用父類的方法時,super會綁定子類的this

也就是說,super.getSex()轉換爲僞代碼就是:

super.getSex.call(this)
// 即
Parent.prototype.getSex.call(this)
複製代碼

(別看這裏扯的多,可是多看點例子🌰的話理解必定會加深入的)

並且super其實還有一個特性,就是你在使用它的時候,必須得顯式的指定它是做爲函數使用仍是對象來使用,不然會報錯的。

好比下面這樣就不能夠:

class Child extends Parent {
    constructor () {
        super() // 不報錯
        super.getSex() // 不報錯
        console.log(super) // 這裏會報錯
    }
}
複製代碼

9.8 題目八

(瞭解extends的繼承目標)

extends後面接着的繼承目標不必定要是個class

class B extends A {},只要A是一個有prototype屬性的函數,就能被B繼承。

因爲函數都有prototype屬性,所以A能夠是任意函數。

來看看這一題:

function Parent () {
  this.name = 'parent'
}

class Child1 extends Parent {}
class Child2 {}
class Child3 extends Array {}
var child1 = new Child1()
var child2 = new Child2()
var child3 = new Child3()
child3[0] = 1

console.log(child1)
console.log(child2)
console.log(child3)
複製代碼

執行結果:

Child1{ name: 'parent' }
Child2{}
Child3[1]
複製代碼
  • 能夠繼承構造函數Parent
  • 不存在任何繼承,就是一個普通的函數,因此直接繼承Function.prototype
  • 能夠繼承原生構造函數

(其實這裏只要做爲一個知道的知識點就能夠了,真正使用來講貌似不經常使用)

總結-class繼承

我滴個乖乖...

class繼承咋有這麼多講的啊。

不過總算是我也說完,你也看完了...

OK👌,來個總結唄。

ES6中的繼承:

  • 主要是依賴extends關鍵字來實現繼承,且繼承的效果相似於寄生組合繼承
  • 使用了extends實現繼承不必定要constructorsuper,由於沒有的話會默認產生並調用它們
  • extends後面接着的目標不必定是class,只要是個有prototype屬性的函數就能夠了

super相關:

  • 在實現繼承時,若是子類中有constructor函數,必須得在constructor中調用一下super函數,由於它就是用來產生實例this的。
  • super有兩種調用方式:當成函數調用和當成對象來調用。
  • super當成函數調用時,表明父類的構造函數,且返回的是子類的實例,也就是此時super內部的this指向子類。在子類的constructorsuper()就至關因而Parent.constructor.call(this)
  • super當成對象調用時,普通函數中super對象指向父類的原型對象,靜態函數中指向父類。且經過super調用父類的方法時,super會綁定子類的this,就至關因而Parent.prototype.fn.call(this)

ES5繼承和ES6繼承的區別:

  • ES5中的繼承(例如構造繼承、寄生組合繼承) ,實質上是先創造子類的實例對象this,而後再將父類的屬性和方法添加到this上(使用的是Parent.call(this))。
  • 而在ES6中卻不是這樣的,它實質是先創造父類的實例對象this(也就是使用super()),而後再用子類的構造函數去修改this

全部繼承總結

唔...寫到最後我感受仍是要將全部的繼承狀況來作一個總結,這邊只總結出實現方式的僞代碼以及原型鏈思惟導圖,具體的優缺點在各個模塊中已經總結好了就不重複了。

1. 原型鏈繼承

僞代碼:

Child.prototype = new Parent()
複製代碼

思惟導圖:

2. 構造繼承

僞代碼:

function Child () {
    Parent.call(this, ...arguments)
}
複製代碼

3. 組合繼承

僞代碼:

// 構造繼承
function Child () {
  Parent.call(this, ...arguments)
}
// 原型鏈繼承
Child.prototype = new Parent()
// 修正constructor
Child.prototype.constructor = Child
複製代碼

思惟導圖:

4. 寄生組合繼承

僞代碼:

// 構造繼承
function Child () {
  Parent.call(this, ...arguments)
}
// 原型式繼承
Child.prototype = Object.create(Parent.prototype)
// 修正constructor
Child.prototype.constructor = Child
複製代碼

思惟導圖:

5. 原型式繼承

僞代碼:

var child = Object.create(parent)
複製代碼

6. 寄生式繼承

僞代碼:

function createAnother (original) {
    var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象
    clone.fn = function () {}; // 以某種方式來加強對象
    return clone; // 返回這個對象
}
複製代碼

7. 混入方式繼承

僞代碼:

function Child () {
    Parent.call(this)
    OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
複製代碼

思惟導圖:

8. class中的繼承

僞代碼:

class Child extends Parent {
    constructor (...args) {
        super(...args)
    }
}
複製代碼

後語

知識無價,支持原創。

參考文章:

你盼世界,我盼望你無bug。這篇文章就介紹到這裏。

其實實現繼承的方式真的有好多種啊~

我在寫以前還考慮要不要把這些狀況都寫進去,由於那樣題目勢必會不少。

可是後來我反思了一下本身

"啪!"

"我提莫在想什麼?"

霖呆呆我出這些題不就是爲了難爲你嘛,那我還在顧慮什麼~

另外細心的小夥伴數了數總題數,這也就只有31道啊,哪來的48道題。

(我把《封裝篇》裏的那17道也算進來了,怎麼滴...你又不是不知道霖呆呆我是標題黨)

如今將題目所有弄懂以後是否是對面向對象以及原型鏈更加熟悉了呢 😁。

沒點讚的小夥伴還請給波贊哦👍,你的每一個贊對我都很重要 😊。

喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai 或者掃一掃下面的二維碼👇👇👇.

我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉

你的鼓勵就是我持續創做的主要動力 😊.

相關推薦:

《全網最詳bpmn.js教材》

《【建議改爲】讀完這篇你還不懂Babel我給你寄口罩》

《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》

《【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理)》

《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》

相關文章
相關標籤/搜索