Javascript繼承知多少
目錄
一、 背景 3
二、 什么是繼承 3
三、 原型鏈繼承 3
(一) 原型鏈概念 3
(二) 默認的原型 5
(三) 判斷實例是否是原型派生的 6
(四) 重寫方法需要在替換原型之后(缺點) 8
(五) 不能使用對象字面量創(chuàng)建原型方法(缺點) 9
(六) 引用類型值帶來的問題(缺點) 10
(七) 不能傳遞參數(shù)(缺點) 11
四、 借用構(gòu)造函數(shù)繼承 12
(一) Call和apply 12
(一) 解決原型鏈繼承的問題(優(yōu)點) 13
(二) 解決原型鏈傳遞參數(shù)的問題(優(yōu)點) 14
(三) 原型屬性和方法無法繼承(缺點) 15
五、 組合繼承 16
(一) 解決原型鏈和構(gòu)造函數(shù)繼承的問題(優(yōu)點) 16
(二) 重復創(chuàng)建實例屬性(缺點) 18
六、 原型式繼承 19
(一) Object.create 20
七、 寄生式繼承 20
八、 寄生組合式繼承 21
九、 類extends繼承 23
十、 總結(jié) 24
(一) 原型鏈 25
(二) 借用構(gòu)造函數(shù) 25
(三) 組合繼承 25
(四) 原型式繼承 26
(五) 寄生式繼承 26
(六) 寄生組合式繼承 26
(七) 類extends繼承 26
ES5 和 ES6繼承區(qū)別 26
十一、 參考文獻 26
一、 背景
JavaScript 的繼承是一個很常見的東西,我們一定要重視它。
它可以用在很多方面,提高代碼復用、開發(fā)規(guī)范和開發(fā)效率。
在面試中經(jīng)常會遇到哦。

二、 什么是繼承
繼承 inheritance 是面向?qū)ο筌浖夹g(shù)當中的一個概念。
這種技術(shù)可以復用以前的代碼,能夠大大縮短開發(fā)周期,降低開發(fā)費用。
繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。
?面向?qū)ο笳Z言都支持兩種繼承方式,分別是 「接口繼承」 和 「實現(xiàn)繼承」。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。ES 只支持實現(xiàn)繼承,主要是依賴原型鏈來實現(xiàn)的。
?
相信你們對下面兩張大學時候老師經(jīng)常拿出來的圖相當熟悉。


animal
那么在 JavaScript 中有哪些方式可以實現(xiàn)繼承呢?
三、 原型鏈繼承
「本質(zhì)其實就是一個類型用另一個類型的實例重寫原型對象」。
(一) 原型鏈簡介
三者關(guān)系:構(gòu)造函數(shù)有個原型對象,原型對象有個指針指向構(gòu)造函數(shù),每個實例都有個內(nèi)部指針指向原型對象。

例子:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 創(chuàng)造實例
let f = new Father()
f.alertName()//father
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c = new Children()
// 兒子就繼承了父親的所有屬性(大別墅),并且獲得了自己的名字
c.alertName()//children
console.log(c.house)//cottage
Father 通過 new 出一個實例賦值給 Children 的原型對象,從而實現(xiàn)了 Children 繼承了 Father。
原型鏈實現(xiàn)繼承的實例、構(gòu)造函數(shù)和原型對象之間的關(guān)系圖:

(二) 默認的原型
所有的引用類型值默認都繼承了 Object,而這個繼承也是通過原型鏈實現(xiàn)的。所有函數(shù)的默認原型都是 Object 的實例,因此默認原型都會包含一個內(nèi)部指針,指向 Object.prototype。
上述例子改一改:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c = new Children()
// 使用 Object 原型上的方法
console.log(c.hasOwnProperty('name'))//true
兒子繼承了父親,父親繼承了Object,所以兒子可以調(diào)用 Object 原型上的方法。
(三) 判斷實例是否是原型派生的
判斷實例和原型的關(guān)系,有兩種方式,分別是 「instanceof」 和 「isPrototypeOf()」 。
可以通過 instanceof 操作符
修改下例子:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c = new Children()
function Other() {
this.name = "other"
}
console.log(c instanceof Children) // true
console.log(c instanceof Father) // true
console.log(c instanceof Object) // true
console.log(c instanceof Other) // false
還可以使用 isPrototypeOf() 方法
代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c = new Children()
function Other() {
this.name = "other"
}
console.log(Children.prototype.isPrototypeOf(c)) // true
console.log(Father.prototype.isPrototypeOf(c)) // true
console.log(Object.prototype.isPrototypeOf(c)) // true
console.log(Other.prototype.isPrototypeOf(c)) // false
從代碼結(jié)果中可以看出:c 是 Children、Father 和 Object 的實例,而不是 Other 的實例。
(四) 重寫方法需要在替換原型之后(缺點)
有些時候子類型需要重寫超類型的方法,如果子類型重寫的方法寫在替換原型之前,那么繼承后的超類型方法會覆蓋子類型定義的方法,重寫無效。所以,子類型重寫方法需要在替換原型之后。
代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 在替換原型后,重寫方法
Children.prototype.alertName = function () {
console.log('在替換原型之后,重寫方法有效')
}
// 創(chuàng)建子實例
let c = new Children()
c.alertName()// 在替換原型之后,重寫方法有效
上述是有效的情況,換一下代碼順序,執(zhí)行后瞬間無效,如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 在替換原型前,重寫方法
Children.prototype.alertName = function () {
console.log('我在替換原型之后,重寫方法有效')
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c = new Children()
c.alertName()// children
(五) 不能使用對象字面量創(chuàng)建原型方法(缺點)
對象字面量是 Object 類型,如果用對象字面量去替換子類型的原型,那么子類型是不繼承超類型,而是直接繼承 Object 類型。
代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
this.house = "cottage"
}
// 原型方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 子構(gòu)造函數(shù)
function Children() {
this.name = "children"
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 添加新方法
Children.prototype = {
speakName: function () {
console.log(this.name)
}
}
// 創(chuàng)建子實例
let c = new Children()
console.log(c instanceof Father)// false
console.log(c instanceof Object)// true
c.alertName()// TypeError: c.alertName is not a function
從結(jié)果中可以看出:c 不是 Father 類型的實例,而是 Object 類型的實例,alertName() 方法定義在 Father 類型上,而不在 Object 上,所以在 children 原型鏈中找不到 alertName() 方法,所以調(diào)用失敗。
所以,千萬別用對象字面量創(chuàng)建原型方法啊。
(六) 引用類型值帶來的問題(缺點)
值類型:字符串string、數(shù)字number、布爾值boolean、null、undefined。
引用類型值:對象object、數(shù)組array、函數(shù)function。
類型中定義了引用類型值,那么繼承得到的所有實例就會共享這個引用類型值。
值類型的繼承不會造成共享,單獨分配一個內(nèi)存空間,代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = "father"
}
// 子構(gòu)造函數(shù)
function Children() {
}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c1 = new Children()
let c2 = new Children()
// 不是引用類型值沒有問題
c1.name = "我修改了name"
console.log(c1.name)// 我修改了name
console.log(c2.name)// father
引用類型的繼承會造成共享,共享一個內(nèi)存空間,代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.child = {
name: 'father'
}
}
// 子構(gòu)造函數(shù)
function Children() {}
// 實現(xiàn)繼承:子構(gòu)造函數(shù)的原型對象=父構(gòu)造函數(shù)的實例對象
Children.prototype = new Father()
// 創(chuàng)建子實例
let c1 = new Children()
let c2 = new Children()
// 是引用類型值就有問題
c1.child.name = "我修改了name"
console.log(c1.child)// { name: '我修改了name' }
console.log(c2.child)// { name: '我修改了name' }
從結(jié)果中可以看到,引用類型繼承后,所有實例都共享一份數(shù)據(jù),c1 改了,c2 沒動,但是打印數(shù)據(jù)卻是一模一樣。
(七) 不能傳遞參數(shù)(缺點)
由于原型中包含引用類型值帶來的問題,在創(chuàng)建子類型的實例中不能像超類型的構(gòu)造函數(shù)中傳遞參數(shù)。給超類型的構(gòu)造函數(shù)傳遞參數(shù)會將繼承這個超類型的子類型所有的實例發(fā)生改變,牽一發(fā)而動全身。
傳參后的問題代碼如下:
// 父構(gòu)造函數(shù)
function Father(name, age) {
this.age = age
this.child = {
name: name
}
}
// 子構(gòu)造函數(shù)
function Children() {}
Children.prototype = new Father('father', 111)
// 創(chuàng)建子實例
let c1 = new Children()
let c2 = new Children()
// 修改引用類型值 name
c1.child.name = '修改引用類型值'
// 修改值類型 age
c1.age = 222
console.log(c1.age)// 222
console.log(c1.child)// { name: '修改引用類型值' }
console.log(c2.age)// 111
console.log(c2.child)// { name: '修改引用類型值' }
從結(jié)果中看出,超類型中給值類型傳參并實現(xiàn)繼承,再修改某個實例的值類型 age 后,兩個實例的值類型 age 值不同。而引用類型相反,修改某個實例的引用類型值后,所有實例的該值都發(fā)生了變化,沒有隔離,共享了一份數(shù)據(jù)。
四、 借用構(gòu)造函數(shù)繼承
利用call和apply方法實現(xiàn)借用構(gòu)造函數(shù)來達到繼承的目的。
(一) Call和apply
先來回顧下 call() 方法
定義:就是用一個指定的 this 和參數(shù)去調(diào)用另一個函數(shù)。接受的參數(shù)必須是列表。this 指的是調(diào)用者, 而不是 fn 的 this 值。如果是非嚴格模式下,null 和 undefined 會變成全局對象。
格式:Fn.call(this,arg1,arg2,……省略n個參數(shù)……)
再來回顧下 apply() 方法,與 call() 方法類似,但是接受的參數(shù)必須是數(shù)組。
格式:Fn.call(this,[ arg1,arg2,……省略n個參數(shù)……])
借用構(gòu)造函數(shù)的本質(zhì)就是利用 call 或者 apply 把父類中通過 this 指定的屬性和方法復制(借用)到子類創(chuàng)建的實例中。因為 this 對象是在運行時基于函數(shù)的執(zhí)行環(huán)境綁定的。
也就是說,this 等于window ,而當函數(shù)被作為某個對象的方法調(diào)用時,this 等于那個對象。call、apply 方法可以用來代替另一個對象調(diào)用一個方法。call、apply 方法可將一個函數(shù)的對象上下文從初始的上下文改變?yōu)?this 指定的新對象。
總之,在子類函數(shù)中,通過 call() 方法調(diào)用超類型后,子類型的實例可以訪問到子類型和超類型中的所有屬性和方法。這樣就實現(xiàn)了子類向父類的繼承,而且還解決了原型對象上對引用類型值的誤修改操作。
(一) 解決原型鏈繼承的問題(優(yōu)點)
由于引用類型值共用同一個內(nèi)存空間,子類型繼承超類型的同時,構(gòu)造出的實例也復制了相同的超類型屬性。改一個實例就等于所有實例發(fā)生了改變。
借用構(gòu)造函數(shù)剛好可以解決這個問題。代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.child = {
name: 'father'
}
}
// 子構(gòu)造函數(shù)
function Children() {
Father.call(this)
}
// 創(chuàng)建子實例
let c1 = new Children()
let c2 = new Children()
// 就算是引用類型值也沒有了問題,太棒了
c1.child.name = "我修改了name"
console.log(c1.child)// { name: '我修改了name' }
console.log(c2.child)// { name: 'father' }
從結(jié)果中可以看到,更改 c1 實例的屬性,c2 實例不受影響。借用構(gòu)造函數(shù)繼承的實例不共享屬性,相互之間不干擾。
(二) 解決原型鏈傳遞參數(shù)的問題(優(yōu)點)
原型鏈無法正常傳遞參數(shù),可以通過借用構(gòu)造函數(shù)方式解決。
代碼如下:
// 父構(gòu)造函數(shù)
function Father(name, age) {
this.age = age
this.child = {
name: name
}
}
// 子構(gòu)造函數(shù)
function Children() {
Father.call(this, 'father', 111)
}
// 創(chuàng)建子實例
let c1 = new Children()
let c2 = new Children()
// 修改引用類型值 name
c1.child.name = '修改引用類型值'
// 修改值類型 age
c1.age = 222
console.log(c1.age)// 222
console.log(c1.child)// { name: '修改引用類型值' }
console.log(c2.age)// 111
console.log(c2.child)// { name: 'father' }
從結(jié)果中可以看到,無論是值類型還是引用類型值,借用構(gòu)造函數(shù)繼承中傳遞參數(shù),然后修改某個實例,并不會應用到所有實例屬性中,每個實例保存自己的那份數(shù)據(jù),具有很好的隔離性。
(三) 原型屬性和方法無法繼承(缺點)
由于不是原型鏈繼承,僅僅是借用構(gòu)造函數(shù),那么無法繼承原型上的屬性和方法。
構(gòu)造函數(shù)中定義的屬性和方法雖然可以訪問,但是每個實例都復制了一份,如果實例過多,方法過多,占用內(nèi)存就大,那么方法都在構(gòu)造函數(shù)中定義,函數(shù)復用就無從談起。
本來我們就是用 prototype 來解決復用問題。
如果方法都作為了實例自己的方法,當需求改變后,要改變其中的一個方法,那么之前的所有實例,其中的方法都不能做出更新,只有后面的實例才能訪問到新方法。
代碼如下:
// 父構(gòu)造函數(shù)
function Father() {
this.name = 'father'
this.speakName1 = function () {
console.log('speakName1')
}
this.speakName2 = function () {
console.log('speakName2')
}
this.speakName3 = function () {
console.log('speakName3')
}
this.speakName4 = function () {
console.log('speakName4')
}
}
// 父原型上 方法
Father.prototype.alertName = function () {
console.log(this.name)
}
// 父原型上 屬性
Father.prototype.age = 21
// 子構(gòu)造函數(shù)
function Children() {
Father.call(this)
}
// 創(chuàng)建子實例
let c1 = new Children()
// 調(diào)用原型方法,實例訪問不到
c1.alertName()
// TypeError: c1.alertName is not a function
// 訪問原型屬性,實例中未定義
console.log(c1.age)
// undefined
// 可以訪問實例屬性,但是每個實例都存有自己一份 name 值
console.log(c1.name)
// father
// 可以訪問實例方法,但是每個實例都存有自己一份 speakName1() 方法,
// 且方法過多,內(nèi)存占用量大,這就不叫復用了
c1.speakName1()// speakName1
c1.speakName2()// speakName2
c1.speakName3()// speakName3
c1.speakName4()// speakName4
// instanceof isPrototypeOf 無法判斷實例和類型的關(guān)系
console.log(Father.prototype.isPrototypeOf(c1))// false
console.log(c1 instanceof Father)// false
從結(jié)果中可以看出,借用構(gòu)造函數(shù)繼承后,實例無法調(diào)用原型方法和訪問屬性,但是可以訪問實例屬性和方法。
而且instanceof 和 isPrototypeOf 無法判斷實例和類型的關(guān)系。
五、 組合繼承
(一) 解決原型鏈和構(gòu)造函數(shù)繼承的問題(優(yōu)點)
上述兩者繼承方式,無論是單獨使用原型鏈繼承還是借用構(gòu)造函數(shù)繼承都有自己的局限性,最好的方式是,將兩者結(jié)合一起使用,發(fā)揮各自的優(yōu)勢。
組合繼承就是將原型鏈繼承和借用構(gòu)造函數(shù)繼承組合起來使用。利用原型鏈繼承繼承原型方法,利用借用構(gòu)造函數(shù)繼承繼承實例屬性。這樣既能實現(xiàn)函數(shù)復用,又能保證每個實例間的屬性不會相互影響。
代碼如下:
// 父構(gòu)造函數(shù)
function Father(name) {
this.child = {
name: name
}
}
// 父原型上綁定方法
Father.prototype.alertName = function () {
console.log(this.child)
}
// 子構(gòu)造函數(shù) 借用構(gòu)造函數(shù)繼承父
function Children(name) {
Father.call(this, name)
}
// 原型鏈繼承
Children.prototype = new Father()
Children.prototype.constructor = Children
// 子原型上創(chuàng)建 函數(shù)
Children.prototype.speakName = function () {
console.log('speakName')
}
// 創(chuàng)建子實例
let c1 = new Children('c1')
let c2 = new Children('c2')
// 修改引用類型值 name
c1.child.name = '修改引用類型值'
// 組合繼承做到了2件事:
// 1.復用原型方法
// 2.實例屬性隔離
c1.alertName()// { name: '修改引用類型值' }
c1.speakName()// speakName
c2.alertName()// { name: 'c2' }
c2.speakName()// speakName
console.log(c1 instanceof Father)//true
很對人可能對「Children.prototype.constructor = Children」這行代碼不明白,飛鴻看前幾遍的時候也是云里霧里,后來明白了。
這一步為創(chuàng)建子類型原型對象添加 constructor 屬性,從而彌補因重寫原型而失去的默認的 constructor 屬性。
(二) 重復創(chuàng)建實例屬性(缺點)
……代碼省略……
// 子構(gòu)造函數(shù) 借用構(gòu)造函數(shù)繼承父
function Children(name) {
Father.call(this, name) //第二次調(diào)用 Father 構(gòu)造函數(shù)
}
// 原型鏈繼承
Children.prototype = new Father() //第一次調(diào)用 Father 構(gòu)造函數(shù)
……代碼省略……
在 new Father() 構(gòu)造實例時,調(diào)用了一次 Father 的構(gòu)造函數(shù)創(chuàng)建了一份實例屬性 child,其綁定在原型對象上。
在 new Children('c') 構(gòu)造實例時,由于原型指向 Father實例,所以又調(diào)用了一次 Father 的構(gòu)造函數(shù)創(chuàng)建了一份實例屬性 child,其綁定在實例上。
如果實例和原型上屬性同名,那么實例上的屬性就屏蔽了原型上的。
那么問題就來了,兩次創(chuàng)建后子類型的原型上和實例對象上都有一份實例屬性。這個不就很影響性能和復用嗎?
六、 原型式繼承
原型式繼承的本質(zhì)就是用一個對象作為另一個對象的基礎(chǔ)。
有這個對象的話,將它傳給 object,然后再根據(jù)具體需求將得到的新對象改一改。代碼如下:
// 基于已有的對象創(chuàng)建新對象,同時還不用創(chuàng)建自定義類型。
function object(obj) {
// 臨時構(gòu)造函數(shù)
function fn() {}
// 傳入的對象替換臨時構(gòu)造函數(shù)的原型
fn.prototype = obj
// 最后返回臨時構(gòu)造函數(shù)的一個實例
return new fn()
}
// father 對象
let father = {
name: 'father',
friend: ['abby', 'bob']
}
// 生成新實例對象 child1
let child1 = object(father)
// 更改值類型屬性
child1.name = '修改了name'
console.log(child1.name) //修改了name
// 更改引用類型值
child1.friend.push('chely')
console.log(child1.friend) //[ 'abby', 'bob', 'chely' ]
// 生成新實例對象 child2
let child2 = object(father)
console.log(child2.name) //father
console.log(child2.friend) //[ 'abby', 'bob', 'chely' ]
從結(jié)果中可以看到,經(jīng)過這個方法的實例對象中的引用類型值和其他實例對象共享,而值類型隔離。實際上創(chuàng)造了兩個 father 對象的副本。
(一) Object.create
ES5 中的 「Object.create()」 方法規(guī)范化了原型式繼承。
Object.create() 方法創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的 「proto」。
提供兩個入?yún)?,第一個是新創(chuàng)建的原型對象;第二個是為新創(chuàng)建的對象添加屬性的對象,同 Object.defineProperties()(看過vue2源碼一定知道)。
代碼如下:
// father 對象
let father = {
name: 'father',
friend: ['abby', 'bob']
}
// 生成新實例對象 child1
let child1 = Object.create(father)
// 更改值類型屬性
child1.name = '修改了name'
console.log(child1.name) //修改了name
// 更改引用類型值
child1.friend.push('chely')
console.log(child1.friend) //[ 'abby', 'bob', 'chely' ]
// 生成新實例對象 child2
let child2 = Object.create(father)
console.log(child2.name) //father
console.log(child2.friend) //[ 'abby', 'bob', 'chely' ]
七、 寄生式繼承
寄生式繼承是基于原型式繼承上修改的,本質(zhì)上就是創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。
缺點就是:使用寄生式繼承來為對象添加函數(shù),會由于不能做到函數(shù)復用而降低效率,同借用構(gòu)造函數(shù)繼承的缺點。
代碼如下:
// father 對象
let father = {
name: 'father',
friend: ['abby', 'bob']
}
function fn(obj) {
let origin = Object.create(obj)
// 繼承了方法,增強了對象,原對象不受影響
origin.alertName = function () {
console.log(this.name)
}
return origin
}
// 生成新實例對象 child1
let child1 = fn(father)
// 更改值類型屬性
child1.name = '修改了name'
console.log(child1.name) //修改了name
// 更改引用類型值
child1.friend.push('chely')
console.log(child1.friend) //[ 'abby', 'bob', 'chely' ]
child1.alertName() // 修改了name
// 生成新實例對象 child2
let child2 = fn(father)
console.log(child2.name) //father
console.log(child2.friend) //[ 'abby', 'bob', 'chely' ]
child2.alertName() //father
八、 寄生組合式繼承
寄生組合式繼承本質(zhì)就是寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型。
通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。
基本思路就是不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需的無非就是超類型的一個副本而已。
寄生組合式繼承是引用類型最理想的繼承范式。
代碼如下:
/**
* 實現(xiàn)了寄生組合式繼承的最簡單形式
* @param {*} children 子類型
* @param {*} father 超類型
*/
function inheritPrototype(children, father) {
// 創(chuàng)建對象:創(chuàng)建超類型的副本
let prototype = Object.create(father.prototype)
// 增強對象:為創(chuàng)建的副本添加constructor屬性,彌補因為重寫原型而失去的默認的constructor的屬性
prototype.constructor = children
// 指定對象:將新創(chuàng)建的副本賦值給子類型的原型
children.prototype = prototype
}
// 父構(gòu)造函數(shù)
function Father(name) {
this.child = {
name: name
}
}
// 父原型上綁定方法
Father.prototype.alertName = function () {
console.log(this.child)
}
// 子構(gòu)造函數(shù) 借用構(gòu)造函數(shù)繼承父
function Children(name) {
Father.call(this, name)
}
inheritPrototype(Children, Father)
// 子原型上創(chuàng)建 函數(shù)
Children.prototype.speakName = function () {
console.log('speakName')
}
// 創(chuàng)建子實例
let c1 = new Children('c1')
let c2 = new Children('c2')
// 修改引用類型值 name
c1.child.name = '修改引用類型值'
c1.alertName()// { name: '修改引用類型值' }
c1.speakName()// speakName
c2.alertName()// { name: 'c2' }
c2.speakName()// speakName
console.log(c1 instanceof Father)//true
九、 類extends繼承
ES6使用 class 利用 「extends」 實現(xiàn)了繼承,原理同寄生組合式繼承。其實就是語法糖。
代碼如下:
// 父類
class Father {
// 父構(gòu)造函數(shù)
constructor(name) {
this.child = {
name: name
}
}
alertName() {
console.log(this.child)
}
}
// 子類繼承父類
class Children extends Father {
constructor(name) {
// super 表示父類的構(gòu)造函數(shù),用來新建父類的 this 對象。
// 因為子類沒有自己的this,必須要調(diào)用super獲取父類的this對象,并加以修改
// 等同于 Father.prototype.constructor.call(this)
// 同時綁定了子類的this
super(name)
}
speakName() {
// 等同于 Father.prototype.alertName() 綁定了子類 Children 的this
super.alertName()
}
}
// 創(chuàng)建子實例
let c1 = new Children('c1')
let c2 = new Children('c2')
// 修改引用類型值 name
c1.child.name = '修改引用類型值'
c1.alertName() //{ name: '修改引用類型值' }
c1.speakName() //{ name: '修改引用類型值' }
c2.alertName() //{ name: 'c2' }
c2.speakName() //{ name: 'c2' }
console.log(c1 instanceof Father) //true
十、 總結(jié)

(一) 原型鏈
本質(zhì):利用超類型的實例替換子類型的原型對象
優(yōu)點:
非常純粹的繼承關(guān)系,實例是子類的實例,也是父類的實例 父類新增原型方法和原型屬性,子類都能訪問到
缺點:
重寫方法需要在替換原型之后 不能使用對象字面量創(chuàng)建原型方法 無法實現(xiàn)多繼承 引用類型被所有實例共享 創(chuàng)建子類實例時,無法向父類構(gòu)造函數(shù)傳參
(二) 借用構(gòu)造函數(shù)
本質(zhì):使用 call() 和 apply() 借用超類型的構(gòu)造函數(shù)來增強子類的實例
優(yōu)點:
解決了原型鏈中,子類實例共享父類引用類型的問題 創(chuàng)建子類實例時,可以向父類傳遞參數(shù) 可以實現(xiàn)多繼承(call多個父類對象)
缺點:
實例并不是父類的實例,只是子類的實例 只能繼承父類的實例屬性和方法,不能繼承原型屬性和原型方法 無法實現(xiàn)函數(shù)復用,每個子類都有父類實例函數(shù)的副本,影響性能
(三) 組合繼承
本質(zhì):將原型鏈和借用構(gòu)造函數(shù)組合
優(yōu)點:
彌補了原型鏈和借用構(gòu)造函數(shù)的缺陷,可以繼承實例屬性和實例方法,也可以繼承原型屬性和原型方法 既是子類的實例,也是父類的實例 不存在引用類型共享問題 可傳參 函數(shù)可復用
缺點:
調(diào)用了兩次的父類函數(shù),有性能問題 由于兩次調(diào)用,會造成實例和原型上有相同的屬性或方法
(四) 原型式繼承
本質(zhì):借用原型以已有對象為基礎(chǔ)創(chuàng)建新對象
優(yōu)點:
不需要單獨創(chuàng)建構(gòu)造函數(shù)
缺點:
多個實例共享被繼承的屬性,存在被篡改的情況 不能傳遞參數(shù)
(五) 寄生式繼承
本質(zhì):創(chuàng)建一個僅用于封裝繼承過程的函數(shù),內(nèi)部增強對象,最后返回對象
優(yōu)點:
只需要關(guān)注對象本身,不在乎類型和構(gòu)造函數(shù)的場景
缺點:
難以復用函數(shù) 無法傳遞參數(shù) 多個實例共享被繼承的屬性,存在被篡改的情況
(六) 寄生組合式繼承
本質(zhì):寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型
(七) 類extends繼承
ES6使用 class 利用 「extends」 實現(xiàn)了繼承,原理同寄生組合式繼承。
ES5 和 ES6繼承區(qū)別
ES5 的繼承本質(zhì)就是先創(chuàng)造子類的實例對象 this,然后再將父類的方法添加到 this 上面(Parent.apply(this))。
ES6的繼承本質(zhì)是先創(chuàng)建父類的實例對象 this(所以必須先調(diào)用 super 方法),然后在使用子類的構(gòu)造函數(shù)修改 this。
作者不才,文中若有錯誤,望請指正,避免誤人子弟。
十一、 參考文獻
《JavaScript高級程序設(shè)計第3版》
《ES6標準入門第3版》
最后,希望大家一定要點贊三連。
?? 看完兩件事
如果你覺得這篇內(nèi)容對你挺有益,我想邀請你幫我兩個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容
關(guān)注公眾號,每周學習一個新技術(shù)
