你盼世界,我盼望你無bug
。Hello 你們好!我是霖呆呆!javascript
怎麼樣?小夥伴們,上一章《封裝篇(牛刀小試)》裏的十幾道題是否是作着不過癮啊。html
心裏活動:就這點水平的東西?還號稱魔鬼題?前端
能夠,小夥子(姑娘),很膨脹,我喜歡。哈哈哈哈。java
既然這樣的話,就來看看這系列的大頭——繼承?es6
這篇文章的繼承題但是有點東西的啊,基本覆蓋了全部主流的繼承狀況,並且都比較細節,若是你原來只是淺淺的看了一些教材,跟着手寫實現了一下而已的話,那你看完保證是會有收穫的!那樣的話還請給個三連哦 😊。面試
☑️點贊➕收藏➕關注編程
❌ 閃現➕大招➕引燃segmentfault
老規矩,不然在評論區給我一個臭臭的👎。全文共有1.7w
字,前先後後整理了快兩個星期(整理真的很容易掉頭髮😂)。數組
因此還請你找個安靜的地方,在一個合適的時間來細細品味它 😊。bash
OK👌,廢話很少說,咱走着,卡加(韓語)~
經過閱讀本篇文章你能夠學習到:
(在正式閱讀本篇文章以前還請先查看封裝篇,也就是目錄的第一章節,以後觀看溫馨感更高哦 😁)
好滴👌,仍是讓咱們先來了解一下繼承的概念哈。
繼承 🤔️?
"嗯...我爸在深圳福田有一套房,之後要繼承給我"
"啪!"
"我提莫的在想什麼?我還有個弟弟,因此我爸得有兩套"
"啪!"
"你提莫還在睡,該搬磚了!"
正經點的,其實一句話來講:
繼承就是子類可使用父類的全部功能,而且對這些功能進行擴展。
好比我有個構造函數A
,而後又有個構造函數B
,可是B
想要使用A
裏的一些屬性和方法,一種辦法就是讓咱們自身化身爲CV俠
,複製粘貼一波。還有一種就是利用繼承,我讓B
直接繼承了A
裏的功能,這樣我就能用它了。
今天要介紹的八種繼承方式在目錄中都已經列舉出來了。
不着急,從淺到深咱一個個來看。
將子類的原型對象指向父類的實例
(理解原型鏈繼承的概念)
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.6
和1.7
同樣(《封裝篇(牛刀小試)》裏的)。
因此如今你知道了吧,這種方式就叫作原型鏈繼承。
將子類的原型對象指向父類的實例。
咱們來寫個僞代碼,方便記憶:
Child.prototype = new Parent() 複製代碼
固然,更加嚴謹一點的作法其實還有一步:Child.prototype.constructor = Child
,不過這邊霖呆呆先賣個關子,到題目4.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()
這種方式。
(理解原型鏈繼承的優勢和缺點)
這道題的結果你們能想到嗎?
請注意對象是地址引用的哦。
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
,而且給colors
和feature
都push
了新的內容。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
打印出了feature
和sex
兩個屬性。(name
和colors
屬於原型對象上的屬性並不會被表現出來)child2
沒有作任何操做,因此它打印出的仍是它自身的一個feature
屬性😁。child1.name
是原型對象parent
上的name
,也就是'parent'
,雖然咱們在new Child
的時候傳遞了'child1'
,但它顯然是無效的,由於接收name
屬性的是構造函數Parent
,而不是Child
。child2.colors
因爲用的也是原型對象parent
上的colors
,又因爲以前被child1
給改變了,因此打印出來的會是['white', 'black', 'yellow']
parent
打印出來,name
和sex
沒變,colors
卻變了。分析的真漂亮,漂亮的這麼一大串我都不想看了...
咳咳,不過你要是能靜下來認真的讀一讀的話就會以爲真沒啥東西,甚至不須要記什麼,我就理解了。
如今咱們就能夠得出原型鏈繼承它的優勢和缺點了
優勢:
缺點:
Child.prototype = new Parent()
這樣的語句後面child1.colors
能夠看出來)child1.name
能夠看出來)這...這看到沒,壓根就不須要記,想一想霖呆呆出的這道變態的題面試的時候被問到脫口就來了。
這道題主要是想介紹一個重要的運算符: 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
這種東西,按照我圖上的查找路線來查找就能夠了 😁 ~
(若是你能看到這裏,你就會發現霖呆呆的美術功底,不是通常的強)
[表情包害羞~]
(瞭解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查找思惟導圖這張圖反過來查找便可。
瞭解了最簡單的原型鏈繼承,再讓咱們來看看構造繼承呀,也叫作構造函數繼承。
在子類構造函數內部使用call或apply
來調用父類構造函數
爲了方便你查看,咱們先來複習一波.call
和apply
方法。
經過call()、apply()
或者bind()
方法直接指定this
的綁定對象, 如foo.call(obj)
使用.call()
或者.apply()
的函數是會直接執行的
而bind()
是建立一個新的函數,須要手動調用纔會執行
.call()
和.apply()
用法基本相似,不過call
接收若干個參數,而apply
接收的是一個數組
(構造繼承的基本原理)
因此來看看這道題?
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
表示的是你能夠往裏面傳遞參數,固然這只是僞代碼)
若是你以爲上面👆這道題還不具備說明性,咱們來看看這裏。
如今我在子類和父類中都加上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') } 複製代碼
這時候就是好孩子了。
(哎,霖呆呆的產生可能就是第二種狀況...)
(構造繼承的優勢)
解決了原型鏈繼承中子類共享父類引用對象的問題
剛剛的題目都是一些基本數據類型,讓我來加上引用類型看看
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
)耶。
這裏的緣由其實咱們前面也說了:
使用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類。
因此如今child1
和child2
如今分別有它們各自的colors
了,就不共享了。
並且這種拷貝屬於深拷貝,驗證的方式是你能夠把colors
數組中的每一項改成一個對象,而後修改它看看。
function Parent () { //... this.colors = [{ title: 'white' }, { title: 'black' }] } 複製代碼
所以咱們能夠得出構造繼承的優勢:
(構造繼承的缺點一)
在瞭解繼承的時候,咱們老是會想到原型鏈上的屬性和方法能不能被繼承到。
採用了這種構造繼承的方式,能不能繼承父類原型鏈上的屬性呢?
來看下面👇這道題目
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')
只不過是讓你複製了一下我構造函數裏的屬性和方法,可沒說能讓你複製我原型對象的啊~年輕人,不要這麼貪嘛。
因此咱們能夠看出構造繼承一個最大的缺點,那就是:
小氣!
"啪!"
"你給我正經點"
😂
實際上是:
"那不就是小氣嘛..."
"..."
(構造繼承的缺點二)
它的第二個缺點是:實例並非父類的實例,只是子類的實例。
停一下,讓咱們先來思考一下這句話的意思,而後想一想怎樣來驗證它呢 🤔️ ?
一分鐘...二分鐘...三分鐘...
啊,我知道了,剛剛不是才學的一個叫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
很好理解啦,我就是你生的,你不true
誰true
false
其實也很好理解啦,想一想剛剛的5.3
,我連你父類原型上的方法都不能用,那我和你可能也沒有關係啦,我只不過是複製了你函數裏的屬性和方法而已。true
,必然的,實例的原型鏈若是沒有發生改變的話最後都能找到Object.prototype
啦。(雖然說構造繼承出來的實例確實不是父類的實例,只是子類的實例。但我實際上是不太明白教材中爲何要說它是一個缺點呢?鄙人愚昧,想的多是:子類生成的實例既然能用到父類中的屬性和方法,那我就應該也要肯定這些屬性和方法的來源,若是不能使用instanceof
檢測到你和父類有關係的話,那就會對這些憑空產生的屬性和方法有所質疑...)
所以構造繼承第二個缺點是:
構造繼承總結來講:
優勢:
3.3
)缺點:
3.4
)3.5
)(最後一個缺點‘沒法實現函數複用’通過評論區小夥伴matteokjh的提醒,我理解的大概是這個意思:父類構造函數中的某個函數可能只是一個功能型的函數,它不論被複制了多少份,輸出的結果或者功能都是同樣的,那麼這類函數是徹底能夠拿來複用的。可是如今用了構造函數繼承,因爲它是複製了父類構造函數中的屬性和方法,這樣產生的每一個子類實例中都會有一份本身各自的方法,但是有的方法徹底沒有必要複製,能夠用來共用的,因此就說不可以「函數複用」。)
既然原型鏈繼承和構造繼承都有這麼多的缺點,那咱們爲什麼不陰陽結合,把它們組合在一塊兒呢?
咦~
好像是個好想法。
把咱們前面的僞代碼拿來用用,想一想該如何組合呢?
// 原型鏈繼承 Child.prototype = new Parent() // 構造繼承 function Child () { Parent.call(this, ...arguments) } 複製代碼
...思考中🤔...
看到這兩段僞代碼,我好像有所頓悟了,不就是按照僞代碼裏寫的,把這兩種繼承組合在一塊兒嗎?
哇!這都被我猜中了,搜索一下組合繼承的概念,果真就是這樣。
組合繼承的概念:
組合繼承就是將原型鏈繼承與構造函數繼承組合在一塊兒,從而發揮二者之長的一種繼承模式。
思路:
基操:
call/apply
在子類構造函數內部調用父類構造函數constructor
屬性,將它指向子類構造函數基操中的第一點就是構造繼承,第二點爲原型鏈繼承,第三點其實只是一個好的慣例,在後面的題目會細講到它。
(理解組合繼承的基本使用)
如今我決定對大家再也不仁慈,讓咱們換種想法,逆向思惟來解解題好很差。
陰笑~
既然我都已經說了這麼多關於組合繼承的東西了,那想必大家也知道該如何設計一個組合繼承了。
我如今須要大家來實現這麼一個Child
和Parent
構造函數(代碼儘量地少),讓它們代碼的執行結果能以下:
(請先不要着急看答案哦,花上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
纔有,代表它是子類構造函數上的定義的屬性(也就是咱們以前提到過的公有屬性)child1
和parent1
均可以調用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
,腦補一下那張原型鏈的圖,是否是這樣呢?
這就是組合繼承帶來的魅力,若是你能看懂這道題,就已經掌握其精髓了 👏。
(理解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.constructor
是Parent
函數這個還好理解,結合上面👆的圖片來看,只要經過原型鏈查找,我parent1
實例自身沒有constructor
屬性,那我就拿原型上的constructor
,發現它指向的是構造函數Parent
,所以第二個打印出Parent
函數。
而對於child1
,想一想組合繼承用到了原型鏈繼承,雖然也用到了構造繼承,可是構造繼承對原型鏈之間的關係沒有影響。那麼我組合繼承的原型鏈關係是否是就能夠用原型鏈繼承那張關係圖來看?
以下:
就像上面看到的同樣,原型鏈繼承切斷了本來Child
和Child
原型對象的關係,而是從新指向了匿名實例。使得實例child1
可以使用匿名實例原型鏈上的屬性和方法。
當咱們想要獲取child1.constructor
,確定是向上查找,經過__proto__
找它構造函數的原型對象匿名實例
。
可是匿名實例它自身是沒有constructor
屬性的呀,它只是Parent
構造函數建立出來的一個對象而已,因此它也會繼續向上查找,而後就找到了Parent
原型對象上的constructor
,也就是Parent
了。
因此回過頭來看看這句話:
construcotr它不過是給咱們一個提示,用來標示實例對象是由哪一個構造函數建立的。
從人(常)性(理)的角度上來看,child1
是Child
構建的,parent1
是Parent
構建的。
那麼child1
它的constructor
就應該是Child
呀,可是如今卻變成了Parent
,貌似並不太符合常理啊。
因此纔有了這麼一句:
Child.prototype.constructor = Child
複製代碼
用以修復constructor
的指向。
如今讓咱們經過改造原型鏈繼承思惟導圖
來畫畫組合繼承的思惟導圖
吧。
(至於爲何在組合繼承中我修復了constructor
,在原型鏈繼承中沒有,這個其實取決於你本身,由於你也看到了constructor
實際並無什麼做用,不過面試被問到的話確定是要知道的)
總結來講:
constructor
它是構造函數原型對象中的一個屬性,正常狀況下它指向的是原型對象。JS
內部屬性,只是用來標示一下某個實例是由哪一個構造函數產生的而已。constructor
的指向,那麼出於編程習慣,咱們最好將它修改成正確的構造函數。(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
對象中有兩個屬性:a
和b
,且值都是1
a.logA()
,打印出的就是a.a
,也就是1
難度升級:
如今我想要在匿名函數外給A
這個構造函數的原型對象中添加一個方法logB
用以打印出this.b
。
你首先想到的是否是B.prototype.logB = funciton() {}
。
可是注意咯,我是要你在匿名函數外添加,而此時因爲做用域的緣由,咱們在匿名函數外是訪問不到A
的,因此這樣的作法就不可行了。
解決辦法:
雖然咱們在外層訪問不到A
,可是咱們能夠經過原型鏈查找,來獲取A
的原型對象呀。
仍是這張圖:
這裏咱們就有兩種解決辦法了:
a.__proto__
來訪問到原型對象:a.__proto__.logB = function () { console.log(this.b) } a.logB() 複製代碼
a.constructor.prototype
來訪問到原型對象:a.constructor.prototype.logB = function () { console.log(this.b) } a.logB() 複製代碼
想一想是否是這樣的?
雖然我a
實例上沒有constructor
,可是原型對象上有呀,因此a.construtor
實際拿的是原型對象上的construtor
。
(我的愚見感受並沒什麼軟用...我用__proto__
就能夠了呀 😂)
(理解組合繼承的優勢)
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 複製代碼
解析思路:
child
的sex
和name
都沒啥問題,而colors
可能會有些疑問,由於colors
是經過構造繼承於父類的,而且是複製出來的屬性,因此改變child1.colors
並不會影響child2.colors
。(相似題目3.3
)Child.prototype
,是使用new Parent
生成的,而且生成的時候是沒有傳遞參數進去的,所以name
和colors
都是undefined
。並且題目中又將constructor
給修正指向了Child
。true
,是由於child1
能夠沿着它的原型鏈查找到Child.prototype
和Parent.prototype
。(相似題目2.1
)如今你就能夠看出組合繼承的優勢了吧,它其實就是將兩種繼承方式的優勢給結合起來。
(理解組合繼承的缺點)
人無完人,狗無完狗,就算是組合繼承這麼牛批的繼承方式也仍是有它的缺點 😁。
一塊兒來看看這裏:
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
上面的了,那你這憑空多出來的屬性不就佔了內存浪費了嗎?
所以咱們能夠看出組合繼承的缺點:
(考察你是否理解實例對象上引用類型和原型對象上引用類型的區別)
這裏可就有一個坑了,得注意了⚠️:
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
是否是確實不是共享的?
那就對了呀,我已經幫你解決了原型(匿名實例
)中引用屬性共享的問題了呀。
至於features
是Parent.prototype
上的屬性,至關因而爺爺那一級別的了,這我可無法子。
一樣的,讓咱們對組合繼承也來作個總結吧:
實現方式:
優勢:
缺點:
constructor總結:
constructor
它是構造函數原型對象中的一個屬性,正常狀況下它指向的是原型對象。JS
內部屬性,只是用來標示一下某個實例是由哪一個構造函數產生的而已。constructor
的指向,那麼出於編程習慣,咱們最好將它修改成正確的構造函數。唔...寄生這個詞聽着有點可怕啊...
它比組合繼承還要牛批一點。
剛剛咱們提了組合繼承的缺點無非就是:
那麼有沒有一種方式讓咱們直接跳過父類實例上的屬性,而讓我直接就能繼承父類原型鏈上的屬性呢?
也就是說,咱們須要一個乾淨的實例對象,來做爲子類的原型。而且這個乾淨的實例對象還得能繼承父類原型對象裏的屬性。
咦~說到乾淨的對象,我就想到了一個方法: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__
。
哈哈哈哈~
這正不是咱們想要的嗎?咱們如今只想要一個乾淨而且能連接到父類原型鏈上的對象。
來看看題目一。
(理解寄生組合繼承的用法)
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()
,多好啊 😁)
雖然寄生組合繼承和組合繼承很是像,不過咱們仍是來看一道題鞏固鞏固吧。
執行結果:
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']
這段代碼以前,child1
和child2
都是共用原型鏈上的features
,可是執行了這段代碼以後,就至關因而給child2
對象上新增了一個名爲features
屬性,因此這時候child2
取的就是它自身的了。(這道題我是使用VSCode
插件Polacode-2019
作的代碼截圖,不知道你們是喜歡這種代碼截圖仍是喜歡源代碼的形式呢?能夠留言告訴霖呆呆 😁)
(另外,關於更多美化工具的使用能夠查看個人這篇文章:你的掘金文章本能夠這麼炫(博客美化工具一波帶走))
寄生組合繼承算是ES6
以前一種比較完美的繼承方式吧。
它避免了組合繼承中調用兩次父類構造函數,初始化兩次實例屬性的缺點。
因此它擁有了上述全部繼承方式的優勢:
instanceOf
和isPrototypeOf
方法算是翻了不少關於JS
繼承的文章吧,其中百分之九十都是這樣介紹原型式繼承的:
該方法的原理是建立一個構造函數,構造函數的原型指向對象,而後調用 new 操做符建立實例,並返回這個實例,本質是一個淺拷貝。
僞代碼以下:
(後面會細講)
function objcet (obj) { function F () {}; F.prototype = obj; F.prototype.constructor = F; return new F(); } 複製代碼
開始覺得是多神祕的東西,但後來真正瞭解了它以後感受用的應該很少吧... 😢
先來看看題目一。
在真正開始看原型式繼承以前,先來看個咱們比較熟悉的東西:
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
的屬性。
不怕你笑話,上面👆說的這種方式就是原型式繼承,只不過在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
)。
優勢:
缺點:
(呀!很久沒用表情包了,此處應該有個表情包)
cccc...
怎麼又來了個什麼寄生式繼承啊,還有完沒完...
心態放平和...
其實這個寄生式繼承也沒啥東西的,它就是在原型式繼承的基礎上再封裝一層,來加強對象,以後將這個對象返回。
來看看僞代碼你就知道了:
function createAnother (original) { var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象 clone.fn = function () {}; // 以某種方式來加強對象 return clone; // 返回這個對象 } 複製代碼
(瞭解寄生式繼承的使用方式)
它的使用方式,唔...
例如我如今想要繼承某個對象上的屬性,同時又想在新建立的對象中新增上一些其它的屬性。
來看下面👇這兩隻貓咪
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: ƒ } {} 複製代碼
實現方式:
優勢:
缺點:
過五關斬六將,咱終於到了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 複製代碼
(理解混入方式繼承的使用)
額,既然您都看到這了,說明實力以及很強了,要不?咱直接就上個複雜點的題?
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'] } 複製代碼
(理解混入方式繼承的原型鏈結構)
同是上面👆的題,我如今多加上幾個輸出:
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 複製代碼
構造函數中主要的幾種繼承方式都已經介紹的差很少了,接下來就讓咱們看看ES6
中class
的繼承吧。
在class
中繼承主要是依靠兩個東西:
extends
super
並且對於該繼承的效果和以前咱們介紹過的寄生組合繼承方式同樣。(沒錯,就是那個最屌的繼承方式)
一塊兒來看看題目一 😁。
(理解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
的繼承方式徹底知足於寄生組合繼承。
(理解extends
的基本做用)
能夠看到上面👆那道題,咱們用到了兩個關鍵的東西:extends
和super
。
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
方法,則會被默認添加constructor
和super
。(理解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
函數。
這樣說來,constructor
和super
是一對好基友啊...
super
函數咱仍是不能省,很重要啊。
而後再看了看它的寫法,有點像是給父級類中傳遞參數的感受啊 😄。
唔...若是你這樣想的話算是猜對了一部分吧。這其實和ES6
的繼承機制有關。
ES5
中的繼承(例如構造繼承、寄生組合繼承) ,實質上是先創造子類的實例對象this
,而後再將父類的屬性和方法添加到this
上(使用的是Parent.call(this)
)。ES6
中卻不是這樣的,它實質是先創造父類的實例對象this
(也就是使用super()
),而後再用子類的構造函數去修改this
。通俗理解就是,子類必須得在constructor
中調用super
方法,不然新建實例就會報錯,由於子類本身沒有本身的this
對象,而是繼承父類的this
對象,而後對其加工,若是不調用super
的話子類就得不到this
對象。
哇哦~
[果真是好基友~]
這道題介紹的是super
的基本做用,下面來講說它的具體用法吧。
(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'
了。child1
和parent1
打印出來,都沒什麼問題。經過這道題咱們能夠看出:
super
當成函數調用時,表明父類的構造函數,且返回的是子類的實例,也就是此時super
內部的this
指向子類。constructor
中super()
就至關因而Parent.constructor.call(this)
(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
當成對象使用的狀況。
(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! 我貌似已經掌握它嘞"
(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) // 這裏會報錯 } } 複製代碼
(瞭解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
繼承咋有這麼多講的啊。
不過總算是我也說完,你也看完了...
OK👌,來個總結唄。
ES6中的繼承:
extends
關鍵字來實現繼承,且繼承的效果相似於寄生組合繼承extends
實現繼承不必定要constructor
和super
,由於沒有的話會默認產生並調用它們extends
後面接着的目標不必定是class
,只要是個有prototype
屬性的函數就能夠了super相關:
constructor
函數,必須得在constructor
中調用一下super
函數,由於它就是用來產生實例this
的。super
有兩種調用方式:當成函數調用和當成對象來調用。super
當成函數調用時,表明父類的構造函數,且返回的是子類的實例,也就是此時super
內部的this
指向子類。在子類的constructor
中super()
就至關因而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
。唔...寫到最後我感受仍是要將全部的繼承狀況來作一個總結,這邊只總結出實現方式的僞代碼以及原型鏈思惟導圖,具體的優缺點在各個模塊中已經總結好了就不重複了。
僞代碼:
Child.prototype = new Parent() 複製代碼
思惟導圖:
僞代碼:
function Child () { Parent.call(this, ...arguments) } 複製代碼
僞代碼:
// 構造繼承 function Child () { Parent.call(this, ...arguments) } // 原型鏈繼承 Child.prototype = new Parent() // 修正constructor Child.prototype.constructor = Child 複製代碼
思惟導圖:
僞代碼:
// 構造繼承 function Child () { Parent.call(this, ...arguments) } // 原型式繼承 Child.prototype = Object.create(Parent.prototype) // 修正constructor Child.prototype.constructor = Child 複製代碼
思惟導圖:
僞代碼:
var child = Object.create(parent) 複製代碼
僞代碼:
function createAnother (original) { var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象 clone.fn = function () {}; // 以某種方式來加強對象 return clone; // 返回這個對象 } 複製代碼
僞代碼:
function Child () { Parent.call(this) OtherParent.call(this) } Child.prototype = Object.create(Parent.prototype) Object.assign(Child.prototype, OtherParent.prototype) Child.prototype.constructor = Child 複製代碼
思惟導圖:
僞代碼:
class Child extends Parent { constructor (...args) { super(...args) } } 複製代碼
知識無價,支持原創。
參考文章:
你盼世界,我盼望你無bug
。這篇文章就介紹到這裏。
其實實現繼承的方式真的有好多種啊~
我在寫以前還考慮要不要把這些狀況都寫進去,由於那樣題目勢必會不少。
可是後來我反思了一下本身
"啪!"
"我提莫在想什麼?"
霖呆呆我出這些題不就是爲了難爲你嘛,那我還在顧慮什麼~
另外細心的小夥伴數了數總題數,這也就只有31
道啊,哪來的48
道題。
(我把《封裝篇》裏的那17
道也算進來了,怎麼滴...你又不是不知道霖呆呆我是標題黨)
如今將題目所有弄懂以後是否是對面向對象以及原型鏈更加熟悉了呢 😁。
沒點讚的小夥伴還請給波贊哦👍,你的每一個贊對我都很重要 😊。
喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai
或者掃一掃下面的二維碼👇👇👇.
我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉
你的鼓勵就是我持續創做的主要動力 😊.
相關推薦:
《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》