JavaScript物件封裝、多型、繼承
前言
你盼世界,我盼望你無bug
。Hello 大家好!我是霖呆呆!
怎麼樣?小夥伴們,上一章《封裝篇(牛刀小試)》裡的十幾道題是不是做著不過癮啊。
內心活動:就這點水平的東西?還號稱魔鬼題?
可以,小夥子(姑娘),很膨脹,我喜歡。
哈哈哈哈。
既然這樣的話,就來看看這系列的大頭——繼承?
這篇文章的繼承題可是有點東西的啊,基本覆蓋了所有主流的繼承情況,而且都比較細節,如果你原來只是淺淺的看了一些教材,跟著手寫實現了一下而已的話,那你看完保證是會有收穫的!那樣的話還請給個三連哦 ?。
☑️點贊➕收藏➕關注
❌ 閃現➕大招➕引燃
老規矩,否則在評論區給我一個臭臭的?。
全文共有1.7w
字,前前後後整理了快兩個星期(整理真的很容易掉頭髮?)。
所以還請你找個安靜的地方,在一個合適的時間來細細品味它 ?。
OK?,廢話不多說,我們走著,卡加(韓語)~
JS繼承系列介紹
通過閱讀本篇文章你可以學習到:
- 封裝
ES6
之前的封裝-建構函式ES6
之後的封裝-class
- 繼承(本篇)
- 原型鏈繼承
- 構造繼承
- 組合繼承
- 寄生組合繼承
- 原型式繼承
- 寄生繼承
- 混入式繼承
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.6
和1.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
,並且給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
可以看出來)
這...這看到沒,壓根就不需要記,想想霖呆呆出的這道變態的題面試的時候被問到脫口就來了。
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
複製程式碼
還不理解?
沒關係,我還有大招:
我在上面?原型鏈繼承的思維導圖上加了三個查詢路線。
被⭕️標記的1、2、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
來呼叫父類建構函式
為了方便你檢視,我們先來複習一波.call
和apply
方法。
-
通過
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
)耶。
這裡的原因其實我們前面也說了:
使用父類的建構函式來增強子類例項,等於是複製父類的例項屬性給子類。
所以現在child1
和child2
現在分別有它們各自的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
很好理解啦,我就是你生的,你不true
誰true
- 第二個為
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 題目一
(理解組合繼承的基本使用)
現在我決定對你們不再仁慈,讓我們換種想法,逆向思維來解解題好不好。
陰笑~
既然我都已經說了這麼多關於組合繼承的東西了,那想必你們也知道該如何設計一個組合繼承了。
我現在需要你們來實現這麼一個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
,腦補一下那張原型鏈的圖,是不是這樣呢?
這就是組合繼承帶來的魅力,如果你能看懂這道題,就已經掌握其精髓了 ?。
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.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
的指向,那麼出於程式設計習慣,我們最好將它修改為正確的建構函式。
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
物件中有兩個屬性: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__
就可以了呀 ?)
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
複製程式碼
解析思路:
- 兩個
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
)
現在你就可以看出組合繼承的優點了吧,它其實就是將兩種繼承方式的優點給結合起來。
- 可以繼承父類例項屬性和方法,也能夠繼承父類原型屬性和方法
- 彌補了原型鏈繼承中引用屬性共享的問題
- 可傳參,可複用
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
是不是確實不是共享的?
那就對了呀,我已經幫你解決了原型(匿名例項
)中引用屬性共享的問題了呀。
至於features
是Parent.prototype
上的屬性,相當於是爺爺那一級別的了,這我可沒法子。
總結-組合繼承
同樣的,讓我們對組合繼承也來做個總結吧:
實現方式:
- 使用原型鏈繼承來保證子類能繼承到父類原型中的屬性和方法
- 使用構造繼承來保證子類能繼承到父類的例項屬性和方法
優點:
- 可以繼承父類例項屬性和方法,也能夠繼承父類原型屬性和方法
- 彌補了原型鏈繼承中引用屬性共享的問題
- 可傳參,可複用
缺點:
- 使用組合繼承時,父類建構函式會被呼叫兩次
- 並且生成了兩個例項,子類例項中的屬性和方法會覆蓋子類原型(父類例項)上的屬性和方法,所以增加了不必要的記憶體。
constructor總結:
constructor
它是建構函式原型物件中的一個屬性,正常情況下它指向的是原型物件。- 它並不會影響任何
JS
內部屬性,只是用來標示一下某個例項是由哪個建構函式產生的而已。 - 如果我們使用了原型鏈繼承或者組合繼承無意間修改了
constructor
的指向,那麼出於程式設計習慣,我們最好將它修改為正確的建構函式。
5. 寄生組合繼承
唔...寄生這個詞聽著有點可怕啊...
它比組合繼承還要牛批一點。
剛剛我們提了組合繼承的缺點無非就是:
- 父類建構函式會被呼叫兩次
- 生成了兩個例項,在父類例項上產生了無用廢棄的屬性
那麼有沒有一種方式讓我們直接跳過父類例項上的屬性,而讓我直接就能繼承父類原型鏈上的屬性呢?
也就是說,我們需要一個乾淨的例項物件,來作為子類的原型。並且這個乾淨的例項物件還得能繼承父類原型物件裡的屬性。
咦~說到乾淨的物件,我就想到了一個方法: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']
這段程式碼之前,child1
和child2
都是共用原型鏈上的features
,但是執行了這段程式碼之後,就相當於是給child2
物件上新增了一個名為features
屬性,所以這時候child2
取的就是它自身的了。
(這道題我是使用VSCode
外掛Polacode-2019
做的程式碼截圖,不知道大家是喜歡這種程式碼截圖還是喜歡原始碼的形式呢?可以留言告訴霖呆呆 ?)
(另外,關於更多美化工具的使用可以檢視我的這篇文章:你的掘金文章本可以這麼炫(部落格美化工具一波帶走))
總結-寄生組合繼承
寄生組合繼承算是ES6
之前一種比較完美的繼承方式吧。
它避免了組合繼承中呼叫兩次父類建構函式,初始化兩次例項屬性的缺點。
所以它擁有了上述所有繼承方式的優點:
- 只呼叫了一次父類建構函式,只建立了一份父類屬性
- 子類可以用到父類原型鏈上的屬性和方法
- 能夠正常的使用
instanceOf
和isPrototypeOf
方法
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中的繼承
建構函式中主要的幾種繼承方式都已經介紹的差不多了,接下來就讓我們看看ES6
中class
的繼承吧。
在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
的基本作用)
可以看到上面?那道題,我們用到了兩個關鍵的東西: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
。
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
函式。
這樣說來,constructor
和super
是一對好基友啊...
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'
了。 - 最後分別將
child1
和parent1
列印出來,都沒什麼問題。
通過這道題我們可以看出:
super
當成函式呼叫時,代表父類的建構函式,且返回的是子類的例項,也就是此時super
內部的this
指向子類。- 在子類的
constructor
中super()
就相當於是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
實現繼承不一定要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
。
所有繼承總結
唔...寫到最後我感覺還是要將所有的繼承情況來做一個總結,這邊只總結出實現方式的虛擬碼以及原型鏈思維導圖,具體的優缺點在各個模組中已經總結好了就不重複了。
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)
}
}
複製程式碼
後語
知識無價,支援原創。
參考文章:
- 《MDN-Object.prototype.hasOwnProperty()》
- 《es6中class類的全方面理解(一)》
- 《瓶子君-JS 繼承的 六 種實現方式》
- 《[js各種方法繼承以及優缺點]》
- 《JS菌-? 一文看懂 JS 繼承》
- 《zhangfaliang-史上最為詳細的javascript繼承》
- 《阮一峰的ES6---Class的繼承》
你盼世界,我盼望你無bug
。這篇文章就介紹到這裡。
其實實現繼承的方式真的有好多種啊~
我在寫之前還考慮要不要把這些情況都寫進去,因為那樣題目勢必會很多。
但是後來我反思了一下自己
"啪!"
"我提莫在想什麼?"
霖呆呆我出這些題不就是為了難為你嘛,那我還在顧慮什麼~
另外細心的小夥伴數了數總題數,這也就只有31
道啊,哪來的48
道題。
(我把《封裝篇》裡的那17
道也算進來了,怎麼滴...你又不是不知道霖呆呆我是標題黨)
現在將題目全部弄懂之後是不是對物件導向以及原型鏈更加熟悉了呢 ?。
沒點讚的小夥伴還請給波贊哦?,你的每個贊對我都很重要 ?。
喜歡霖呆呆的小夥還希望可以關注霖呆呆的公眾號 LinDaiDai
或者掃一掃下面的二維碼???.
我會不定時的更新一些前端方面的知識內容以及自己的原創文章?
你的鼓勵就是我持續創作的主要動力 ?.
相關推薦:
《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》