夯基礎(chǔ)之手撕javascript繼承詳解
前言
提到JS繼承,你首先想到的什么? 面試 繼承方式 優(yōu)缺點(diǎn)...,js繼承作為曾經(jīng)的苦主,我看了忘,忘了看,看了又忘,OMG,都9012年了面試官還不放過(guò)我。
ok,開(kāi)開(kāi)玩笑,接下來(lái)言歸正傳,來(lái)聊聊js繼承這個(gè)經(jīng)典的話題。
JS的“類”
javascript不像java,php等傳統(tǒng)的OOP語(yǔ)言,js本身并沒(méi)有類這個(gè)概念,那么它是怎么實(shí)現(xiàn)類的模擬呢?
- 構(gòu)造函數(shù)方式
- 原型方式
- 混合方式
構(gòu)造函數(shù)方式
Function Foo (name) {
this.name = name
this.like = function () {
console.log(`like${this.name}`)
}
}
let foo = new Foo('bibidong')
像這樣就是通過(guò)構(gòu)造函數(shù)的方式來(lái)定義類,其實(shí)和普通函數(shù)一樣,但為了和常規(guī)函數(shù)有個(gè)區(qū)分,一般把函數(shù)名首字母大寫。
缺點(diǎn):無(wú)法共享類的方法。
原型方式
function Foo (name) {}
Foo.prototype.color = 'red'
Foo.prototype.queue = [1,2,3]
let foo1 = new Foo()
let foo2 = new Foo()
foo1.queue.push(4)
console.log(foo1) // [1, 2, 3, 4]
console.log(foo2) // [1, 2, 3, 4]
我們通過(guò)原型方式直接把屬性和方法定義在了構(gòu)造函數(shù)的原型對(duì)象上,實(shí)例可以共享這些屬性和方法,解決了構(gòu)造函數(shù)方式定義類的缺點(diǎn)。
缺點(diǎn):可以看到我們改變了foo1的數(shù)據(jù),結(jié)果foo2的queue屬性也變了,這便是原型方式最大的問(wèn)題,引用類型的屬性會(huì)被其它實(shí)例修改。除此之外,這種方式下也無(wú)法傳參。
混合方式
function Foo (name) { // 屬性定義在構(gòu)造函數(shù)里面
this.name = name
this.color = 'red'
this.queue = [1,2,3]
}
Foo.prototype.like = function () { // 方法定義在原型上
console.log(`like${this.name}`)
}
let foo1 = new Foo()
let foo2 = new Foo()
所謂混合模式,便是把上面兩種方式混合起來(lái),我們?cè)跇?gòu)造函數(shù)里面定義屬性,在原型對(duì)象上定義要共享的方法,既能傳參,也避免了原型模式的問(wèn)題。
小結(jié)一下:js類的能力是模擬出來(lái)的,可以通過(guò)構(gòu)造函數(shù)方式,原型方式來(lái)定義,混合模式則聚合了前兩者的優(yōu)點(diǎn)。除此,還有Object.create(), es6的class,都可以來(lái)創(chuàng)建對(duì)象,定義類。
常見(jiàn)的繼承方式
一、原型鏈繼承
基于原型鏈查找的特點(diǎn),我們將父類的實(shí)例作為子類的原型,這種繼承方式便是原型鏈繼承。
function Parent () {
this.color = 'red'
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log('')
}
function Child () { }
Child.prototype = new Parent() // constructor指針變了 指向了Parent
Child.prototype.constructor = Child // 手動(dòng)修復(fù)
let child = new Child()
Child.prototype相當(dāng)于是父類Parent的實(shí)例,父類Parent的實(shí)例屬性被掛到了子類的原型對(duì)象上面,拿color屬性舉個(gè)例子,相當(dāng)于就是這樣
Child.prototype.color = 'red'
這樣父類的實(shí)例屬性都被共享了,我們打印一下child,可以看到child沒(méi)有自己的實(shí)例屬性,它訪問(wèn)的是它的原型對(duì)象。

我們創(chuàng)建兩個(gè)實(shí)例child1,child2
let child1 = new Child() let child2 = new Child() child1.color = 'bulr' console.log(child1) console.log(child2)

我們修改了child1的color屬性,child2沒(méi)有受到影響,并非是其它實(shí)例擁有獨(dú)立的color屬性,而是因?yàn)檫@個(gè)color屬性直接添加到了child1上面,它原型上的color并沒(méi)有動(dòng),所以其它實(shí)例不會(huì)受到影響從打印結(jié)果也可以清楚看到這一點(diǎn)。那如果我們修改的屬性是個(gè)引用類型呢?
child1.queue = [1,2,3,'我被修改了'] // 重新賦值
child1.like = function () {console.log('like方法被我修改了')}
console.log(child1)
console.log(child2)

我們重寫了引用類型的queue屬性和like方法,其實(shí)和修改color屬性是完全一樣的,它們都直接添加到了child1的實(shí)例屬性上。從打印結(jié)果能看到這兩個(gè)屬性已經(jīng)添加到了child1上了,而child2并不會(huì)受到影響,再來(lái)看下面這個(gè)。
child1.queue.push('add push') // 這次沒(méi)有重新賦值
console.log(child1)
console.log(child2)

如果進(jìn)行了重新賦值,會(huì)添加到到實(shí)例屬性上,和原型上到同名屬性便無(wú)關(guān)了,所以并不會(huì)影響到原型。這次我們采用push方法,沒(méi)有開(kāi)辟新空間,修改的就是原型。child2的queue屬性變化了,子類Child原型上的queue屬性被實(shí)例修改,這樣肯定就影響到了所有實(shí)例。
缺點(diǎn)
- 子類的實(shí)例會(huì)共享父類構(gòu)造函數(shù)引用類型的屬性
- 創(chuàng)建子類實(shí)例的時(shí)候無(wú)法傳參
二、構(gòu)造函數(shù)式繼承
相當(dāng)于拷貝父類的實(shí)例屬性給子類,增強(qiáng)了子類構(gòu)造函數(shù)的能力
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`like${this.name}`)
}
function Child (name) {
Parent.call(this, name) // 核心代碼
}
let child = new Child(1)

我們打印了一下child,可以看到子類擁有父類的實(shí)例屬性和方法,但是child的__proto__上面沒(méi)有父類的原型對(duì)象。解決了原型鏈的兩個(gè)問(wèn)題(子類實(shí)例的各個(gè)屬性相互獨(dú)立、還能傳參)
缺點(diǎn)
- 子類無(wú)法繼承父類原型上面的方法和屬性。
- 在構(gòu)造函數(shù)中定義的方法,每次創(chuàng)建實(shí)例都會(huì)再創(chuàng)建一遍。
三、組合繼承
人如其名,組合組合,一定把什么東西組合起來(lái)。沒(méi)錯(cuò),組合繼承便是把上面兩種繼承方式進(jìn)行組合。
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`like${this.name}`)
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child // 修復(fù)constructor指針
let child = new Child('')
接下來(lái)我們做點(diǎn)什么,看它組合后能不能把原型鏈繼承和構(gòu)造函數(shù)繼承的優(yōu)點(diǎn)發(fā)揚(yáng)光大
let child1 = new Child('bibidong')
let child2 = new Child('huliena')
child1.queue.push('add push')
console.log(child1)
console.log(child2)

我們更新了child1的引用屬性,發(fā)現(xiàn)child2實(shí)例沒(méi)受到影響,原型上的like方法也在,不錯(cuò),組合繼承確實(shí)將二者的優(yōu)點(diǎn)發(fā)揚(yáng)光大了,解決了二者的缺點(diǎn)。組合模式下,通常在構(gòu)造函數(shù)上定義實(shí)例屬性,在原型對(duì)象上定義要共享的方法,通過(guò)原型鏈繼承方法讓子類繼承父類構(gòu)造函數(shù)原型上的方法,通過(guò)構(gòu)造函數(shù)繼承方法子類得以繼承構(gòu)造函數(shù)的實(shí)例屬性,是一種功能上較完美的繼承方式。
缺點(diǎn):父類構(gòu)造函數(shù)被調(diào)用了兩次,第一次調(diào)用后,子類的原型上擁有了父類的實(shí)例屬性,第二次call調(diào)用復(fù)制了一份父類的實(shí)例屬性作為子類Child的實(shí)例屬性,那么子類原型上的同名屬性就被覆蓋了。雖然被覆蓋了功能上沒(méi)什么大問(wèn)題,但這份多余的同名屬性一直存在子類原型上,如果我們刪除實(shí)例上的這個(gè)屬性,實(shí)際上還能訪問(wèn)到,此時(shí)獲取到的是它原型上的屬性。
Child.prototype = new Parent() // 第一次構(gòu)建原型鏈 Parent.call(this, name) // 第二次new操作符內(nèi)部通過(guò)call也執(zhí)行了一次父類構(gòu)造函數(shù)
四、原型式繼承
將一個(gè)對(duì)象作為基礎(chǔ),經(jīng)過(guò)處理得到一個(gè)新對(duì)象,這個(gè)新對(duì)象會(huì)將原來(lái)那個(gè)對(duì)象作為原型,這種繼承方式便是原型式繼承,一句話總結(jié)就是將傳入的對(duì)象作為要?jiǎng)?chuàng)建的新對(duì)象的原型。
先寫下這個(gè)有處理能力的函數(shù)
function prodObject (obj) {
function F (){
}
F.prototype = obj
return new F() // 返回一個(gè)實(shí)例對(duì)象
}
這也是Object.create()的實(shí)現(xiàn)原理,所以用Object.create直接替換prodObject函數(shù)是ok的
這也是Object.create()的實(shí)現(xiàn)原理,所以用Object.create直接替換prodObject函數(shù)是ok的
let base = {
color: 'red',
queue: [1, 2, 3]
}
let child1 = prodObject(base)
let child2 = prodObject(base)
console.log(child1)
console.log(child2)

原型式繼承基于prototype,和原型鏈繼承類似,這種繼承方式下實(shí)例沒(méi)有自己的屬性值,訪問(wèn)到也是原型上的屬性。
缺點(diǎn):同原型鏈繼承
五、寄生式繼承
原型式繼承的升級(jí),寄生繼承封裝了一個(gè)函數(shù),在內(nèi)部增強(qiáng)了原型式繼承產(chǎn)生的對(duì)象。
function greaterObject (obj) {
let clone = prodObject(obj)
clone.queue = [1, 2, 3]
clone.like = function () {}
return clone
}
let parent = {
name: 'bibidong',
color: ['red', 'bule', 'black']
}
let child = greaterObject(parent)
打印了一下child,它的缺點(diǎn)也很明顯了,寄生式繼承增強(qiáng)了對(duì)象,卻也無(wú)法避免原型鏈繼承的問(wèn)題。
缺點(diǎn)
- 擁有原型鏈繼承的缺點(diǎn)
- 除此,內(nèi)部的函數(shù)無(wú)法復(fù)用
六、寄生組合式繼承
大招來(lái)了,寄生組合閃亮登場(chǎng)!
上面說(shuō)到,組合繼承的問(wèn)題在于會(huì)調(diào)用二次父類,造成子類原型上產(chǎn)生多余的同名屬性。Child.prototype = new Parent(),那這行代碼該怎么改造呢?
我們的目的是要讓父類的實(shí)例屬性不出現(xiàn)在子類原型上,如果讓Child.prototype = Parent.prototype,這樣不就能保證子類只掛載父類原型上的方法,實(shí)例屬性不就沒(méi)了嗎,代碼如下,看起來(lái)好像是簡(jiǎn)直不要太妙啊。
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`like${this.name}`)
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = Parent.prototype // 只改寫了這一行
Child.prototype.constructor = Child
let child = new Child('')
回過(guò)神突然發(fā)現(xiàn)改寫的那一行如果Child.prototype改變了,那豈不是直接影響到了父類,舉個(gè)栗子
Child.prototype.addByChild = function () {}
Parent.prototype.hasOwnProperty('addByChild') // true
addByChild方法也被加到了父類的原型上,所以這種方法不夠優(yōu)雅。同樣還是那一行,直接訪問(wèn)到Parent.prototype存在問(wèn)題,那我們可以產(chǎn)生一個(gè)以Parent.prototype作為原型的新對(duì)象,這不就是上面原型式繼承的處理函數(shù)prodObject嗎
Child.prototype = Object.create(Parent.prototype) // 改為這樣
這樣就解決了所有問(wèn)題,我們怕改寫Child.prototype影響父類,通過(guò)Object.create返回的實(shí)例對(duì)象,我們將Child.prototype間接指向Parent.prototype,當(dāng)再增加addByChild方法時(shí),屬性就和父類沒(méi)關(guān)系了。
寄生組合式繼承也被認(rèn)為是最完美的繼承方式,最推薦使用。
總結(jié)
js的繼承方式主要就這六種,es6的繼承是個(gè)語(yǔ)法糖,本質(zhì)也是基于寄生組合。這六種繼承方式,其中原型鏈繼承和構(gòu)造函數(shù)繼承最為基礎(chǔ)和經(jīng)典,組合繼承聚合了它們二者的能力,但在某些情況下會(huì)造成錯(cuò)誤。原型式繼承和原型鏈相似,寄生式繼承是在原型式繼承基礎(chǔ)上變化而來(lái),它增強(qiáng)了原型式繼承的能力。最后的寄生組合繼承解決了組合繼承的問(wèn)題,是一種最為理想的繼承方式。
到此這篇關(guān)于夯基礎(chǔ)之手撕javascript繼承的文章就介紹到這了,更多相關(guān)手撕js繼承內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS+CSS實(shí)現(xiàn)Div彈出窗口同時(shí)背景變暗的方法
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)Div彈出窗口同時(shí)背景變暗的方法,是一款比較典型的javascript操作彈出窗口的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03
javascript 獲取多條數(shù)據(jù)(模擬ajax獲取數(shù)據(jù))
javascript 獲取多條數(shù)據(jù)(模擬ajax獲取數(shù)據(jù)),這樣的好處不用額外的ajax讀取鏈接,減輕服務(wù)器負(fù)擔(dān)。2009-06-06
layui 解決富文本框form表單提交為空的問(wèn)題
今天小編就為大家分享一篇layui 解決富文本框form表單提交為空的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10
抓取JavaScript動(dòng)態(tài)加載的內(nèi)容的方法總結(jié)
JavaScript動(dòng)態(tài)加載的內(nèi)容常見(jiàn)于現(xiàn)代Web應(yīng)用中,用于增強(qiáng)用戶體驗(yàn)和減少初始頁(yè)面加載時(shí)間,然而,這些動(dòng)態(tài)加載的內(nèi)容對(duì)于傳統(tǒng)的網(wǎng)頁(yè)抓取工具來(lái)說(shuō)往往是不可見(jiàn)的,本文主要介紹了有JavaScript動(dòng)態(tài)加載的內(nèi)容如何抓取,需要的朋友可以參考下2024-09-09
IE9+已經(jīng)不對(duì)document.createElement向下兼容的解決方法
這篇文章主要介紹了IE9+已經(jīng)不對(duì)document.createElement向下兼容的解決方法,需要的朋友可以參考下2015-09-09
簡(jiǎn)單談?wù)刯avascript代碼復(fù)用模式
這篇文章主要簡(jiǎn)單談?wù)刯avascript代碼復(fù)用模式,主要詳細(xì)介紹了類式繼承模式中的默認(rèn)模式,希望大家能夠喜歡。2015-01-01

