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

LinDaiDai_霖呆呆發表於2020-03-21

JavaScript物件封裝、多型、繼承

前言

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

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

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

可以,小夥子(姑娘),很膨脹,我喜歡。

哈哈哈哈。

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

這篇文章的繼承題可是有點東西的啊,基本覆蓋了所有主流的繼承情況,而且都比較細節,如果你原來只是淺淺的看了一些教材,跟著手寫實現了一下而已的話,那你看完保證是會有收穫的!那樣的話還請給個三連哦 ?。

☑️點贊➕收藏➕關注

❌ 閃現➕大招➕引燃

老規矩,否則在評論區給我一個臭臭的?。

全文共有1.7w字,前前後後整理了快兩個星期(整理真的很容易掉頭髮?)。

所以還請你找個安靜的地方,在一個合適的時間來細細品味它 ?。

OK?,廢話不多說,我們走著,卡加(韓語)~

JS繼承系列介紹

通過閱讀本篇文章你可以學習到:

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

(在正式閱讀本篇文章之前還請先檢視封裝篇,也就是目錄的第一章節,之後觀看舒適感更高哦 ?)

繼承

好滴?,還是讓我們先來了解一下繼承的概念哈。

繼承 ?️?

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

"啪!"

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

"啪!"

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

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

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

繼承就是子類可以使用父類的所有功能,並且對這些功能進行擴充套件。

比如我有個建構函式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一樣(《封裝篇(牛刀小試)》裡的)。

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

所以現在你知道了吧,這種方式就叫做原型鏈繼承

將子類的原型物件指向父類的例項。

我們來寫個虛擬碼,方便記憶:

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卻變了。

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

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

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

總結-原型鏈繼承

現在我們就可以得出原型鏈繼承它的優點和缺點了

優點:

  • 繼承了父類的模板,又繼承了父類的原型物件

缺點:

  • 如果要給子類的原型上新增屬性和方法,就必須放在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的原型物件。

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

好滴,一張圖簡潔明瞭。以後再碰到instanceof這種東西,按照我圖上的查詢路線來查詢就可以了 ? ~

(如果你能看到這裡,你就會發現霖呆呆的美術功底,不是一般的強)

[表情包害羞~]

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

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')只不過是讓你複製了一下我建構函式裡的屬性和方法,可沒說能讓你複製我原型物件的啊~年輕人,不要這麼貪嘛。

所以我們可以看出構造繼承一個最大的缺點,那就是:

小氣!

"啪!"

"你給我正經點"?

其實是:

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

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

"..."

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

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它存在的位置吧:

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

可以看到,它實際就是原型物件上的一個屬性,指向的是建構函式。

所以我們是不是可以有這麼一層對應關係:

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,想想組合繼承用到了原型鏈繼承,雖然也用到了構造繼承,但是構造繼承對原型鏈之間的關係沒有影響。那麼我組合繼承的原型鏈關係是不是就可以用原型鏈繼承那張關係圖來看?

如下:

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

就像上面看到的一樣,原型鏈繼承切斷了原本ChildChild原型物件的關係,而是重新指向了匿名例項。使得例項child1能夠使用匿名例項原型鏈上的屬性和方法。

當我們想要獲取child1.constructor,肯定是向上查詢,通過__proto__找它建構函式的原型物件匿名例項

但是匿名例項它自身是沒有constructor屬性的呀,它只是Parent建構函式建立出來的一個物件而已,所以它也會繼續向上查詢,然後就找到了Parent原型物件上的constructor,也就是Parent了。

所以回過頭來看看這句話:

construcotr它不過是給我們一個提示,用來標示例項物件是由哪個建構函式建立的。

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

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

所以才有了這麼一句:

Child.prototype.constructor = Child
複製程式碼

用以修復constructor的指向。

現在讓我們通過改造原型鏈繼承思維導圖來畫畫組合繼承的思維導圖吧。

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

(至於為什麼在組合繼承中我修復了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的原型物件呀。

還是這張圖:

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

這裡我們就有兩種解決辦法了:

  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上的屬性,相當於是爺爺那一級別的了,這我可沒法子。

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

總結-組合繼承

同樣的,讓我們對組合繼承也來做個總結吧:

實現方式:

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

優點:

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

缺點:

  • 使用組合繼承時,父類建構函式會被呼叫兩次
  • 並且生成了兩個例項,子類例項中的屬性和方法會覆蓋子類原型(父類例項)上的屬性和方法,所以增加了不必要的記憶體。

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的。

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

(靈魂畫手再次上線)

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

可以看到,現在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

最後的三個空物件,我們就需要展開來看看了:

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

  • 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 題目二

雖然寄生組合繼承組合繼承非常像,不過我們還是來看一道題鞏固鞏固吧。

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

執行結果:

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的屬性。

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

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)
複製程式碼

執行結果為:

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

效果是和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)。

優點:

  • 再不用建立建構函式的情況下,實現了原型鏈繼承,程式碼量減少一部分。

缺點:

  • 一些引用資料操作的時候會出問題,兩個例項會公用繼承例項的引用資料類
  • 謹慎定義方法,以免定義方法也繼承物件原型的方法重名
  • 無法直接給父級建構函式使用引數

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

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

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的畫好了:

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

可以看到,其實它與前面我們畫的寄生組合繼承思維導圖就多了下面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繼承

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

寄生組合繼承

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

可以看到,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()
複製程式碼

其實這裡的執行結果和沒有隱去之前一樣。

執行結果:

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

那我們是不是可以認為:

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! 我貌似已經掌握它嘞"

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

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

所有繼承總結

唔...寫到最後我感覺還是要將所有的繼承情況來做一個總結,這邊只總結出實現方式的虛擬碼以及原型鏈思維導圖,具體的優缺點在各個模組中已經總結好了就不重複了。

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

1. 原型鏈繼承

虛擬碼:

Child.prototype = new Parent()
複製程式碼

思維導圖:

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

2. 構造繼承

虛擬碼:

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

3. 組合繼承

虛擬碼:

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

思維導圖:

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

4. 寄生組合繼承

虛擬碼:

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

思維導圖:

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

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
複製程式碼

思維導圖:

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

8. class中的繼承

虛擬碼:

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

後語

知識無價,支援原創。

參考文章:

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

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

我在寫之前還考慮要不要把這些情況都寫進去,因為那樣題目勢必會很多。

但是後來我反思了一下自己

"啪!"

"我提莫在想什麼?"

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

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

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

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

現在將題目全部弄懂之後是不是對物件導向以及原型鏈更加熟悉了呢 ?。

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

喜歡霖呆呆的小夥還希望可以關注霖呆呆的公眾號 LinDaiDai 或者掃一掃下面的二維碼???.

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

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

你的鼓勵就是我持續創作的主要動力 ?.

相關推薦:

《全網最詳bpmn.js教材》

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

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

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

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

相關文章