Javascript繼承知多少

語言: CN / TW / HK


theme: cyanosis highlight: vs2015


一、 背景

JavaScript 的繼承是一個很常見的東西,我們一定要重視它。

它可以用在很多方面,提高程式碼複用、開發規範和開發效率。

在面試中經常會遇到哦。

本文首發於公眾號「前端keep」,歡迎關注。

二、 什麼是繼承

繼承 inheritance 是面向物件軟體技術當中的一個概念。

這種技術可以複用以前的程式碼,能夠大大縮短開發週期,降低開發費用。

繼承就是子類繼承父類的特徵和行為,使得子類物件(例項)具有父類的例項和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

面嚮物件語言都支援兩種繼承方式,分別是 介面繼承實現繼承。介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。ES 只支援實現繼承,主要是依賴原型鏈來實現的。

相信你們對下面兩張大學時候老師經常拿出來的圖相當熟悉。

cat.png

animal.png

那麼在 JavaScript 中有哪些方式可以實現繼承呢?

三、 原型鏈繼承

本質其實就是一個型別用另一個型別的例項重寫原型物件

(一) 原型鏈簡介

三者關係:建構函式有個原型物件,原型物件有個指標指向建構函式,每個例項都有個內部指標指向原型物件。

原型鏈.png

例子: ```js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 創造例項 let f = new Father()

f.alertName()//father

// 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 let c = new Children() // 兒子就繼承了父親的所有屬性(大別墅),並且獲得了自己的名字

c.alertName()//children

console.log(c.house)//cottage ``` Father 通過 new 出一個例項賦值給 Children 的原型物件,從而實現了 Children 繼承了 Father。

原型鏈實現繼承的例項、建構函式和原型物件之間的關係圖:

原型鏈繼承.png

(二) 預設的原型

所有的引用型別值預設都繼承了 Object,而這個繼承也是通過原型鏈實現的。所有函式的預設原型都是 Object 的例項,因此預設原型都會包含一個內部指標,指向 Object.prototype。

上述例子改一改: js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 let c = new Children() // 使用 Object 原型上的方法 console.log(c.hasOwnProperty('name'))//true 兒子繼承了父親,父親繼承了Object,所以兒子可以呼叫 Object 原型上的方法。

(三) 判斷例項是否是原型派生的

判斷例項和原型的關係,有兩種方式,分別是 instanceofisPrototypeOf()

可以通過 instanceof 操作符

修改下例子: ```js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 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() 方法

程式碼如下: ```js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 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 ``` 從程式碼結果中可以看出:c 是 Children、Father 和 Object 的例項,而不是 Other 的例項。

(四) 重寫方法需要在替換原型之後(缺點)

有些時候子型別需要重寫超型別的方法,如果子型別重寫的方法寫在替換原型之前,那麼繼承後的超型別方法會覆蓋子型別定義的方法,重寫無效。所以,子型別重寫方法需要在替換原型之後。

程式碼如下: js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 在替換原型後,重寫方法 Children.prototype.alertName = function () { console.log('在替換原型之後,重寫方法有效') } // 建立子例項 let c = new Children() c.alertName()// 在替換原型之後,重寫方法有效 上述是有效的情況,換一下程式碼順序,執行後瞬間無效,如下: js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 在替換原型前,重寫方法 Children.prototype.alertName = function () { console.log('我在替換原型之後,重寫方法有效') } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 let c = new Children() c.alertName()// children

(五) 不能使用物件字面量建立原型方法(缺點)

物件字面量是 Object 型別,如果用物件字面量去替換子型別的原型,那麼子型別是不繼承超型別,而是直接繼承 Object 型別。

程式碼如下: ```js // 父建構函式 function Father() { this.name = "father" this.house = "cottage" } // 原型方法 Father.prototype.alertName = function () { console.log(this.name) } // 子建構函式 function Children() { this.name = "children" } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 新增新方法 Children.prototype = { speakName: function () { console.log(this.name) } } // 建立子例項 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 ``` 從結果中可以看出: c 不是 Father 型別的例項,而是 Object 型別的例項,alertName() 方法定義在 Father 型別上,而不在 Object 上,所以在 children 原型鏈中找不到 alertName() 方法,所以呼叫失敗。

所以,千萬別用物件字面量建立原型方法啊。

(六) 引用型別值帶來的問題(缺點)

值型別:字串string、數字number、布林值boolean、null、undefined。

引用型別值:物件object、陣列array、函式function。

型別中定義了引用型別值,那麼繼承得到的所有例項就會共享這個引用型別值。

值型別的繼承不會造成共享,單獨分配一個記憶體空間,程式碼如下: ```js // 父建構函式 function Father() { this.name = "father" } // 子建構函式 function Children() { } // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 let c1 = new Children() let c2 = new Children()

// 不是引用型別值沒有問題 c1.name = "我修改了name" console.log(c1.name)// 我修改了name

console.log(c2.name)// father 引用型別的繼承會造成共享,共享一個記憶體空間,程式碼如下:js // 父建構函式 function Father() { this.child = { name: 'father' } } // 子建構函式 function Children() {} // 實現繼承:子建構函式的原型物件=父建構函式的例項物件 Children.prototype = new Father() // 建立子例項 let c1 = new Children() let c2 = new Children()

// 是引用型別值就有問題 c1.child.name = "我修改了name" console.log(c1.child)// { name: '我修改了name' }

console.log(c2.child)// { name: '我修改了name' } ``` 從結果中可以看到,引用型別繼承後,所有例項都共享一份資料,c1 改了,c2 沒動,但是列印資料卻是一模一樣。

(七) 不能傳遞引數(缺點)

由於原型中包含引用型別值帶來的問題,在建立子型別的例項中不能像超型別的建構函式中傳遞引數。給超型別的建構函式傳遞引數會將繼承這個超型別的子型別所有的例項發生改變,牽一髮而動全身。

傳參後的問題程式碼如下: ```js // 父建構函式 function Father(name, age) { this.age = age this.child = { name: name } } // 子建構函式 function Children() {}

Children.prototype = new Father('father', 111)

// 建立子例項 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: '修改引用型別值' } ``` 從結果中看出,超型別中給值型別傳參並實現繼承,再修改某個例項的值型別 age 後,兩個例項的值型別 age 值不同。而引用型別相反,修改某個例項的引用型別值後,所有例項的該值都發生了變化,沒有隔離,共享了一份資料。

四、 借用建構函式繼承

利用call和apply方法實現借用建構函式來達到繼承的目的。

(一) Call和apply

先來回顧下 call() 方法

定義:就是用一個指定的 this 和引數去呼叫另一個函式。接受的引數必須是列表。this 指的是呼叫者, 而不是 fn 的 this 值。如果是非嚴格模式下,null 和 undefined 會變成全域性物件。

格式:Fn.call(this,arg1,arg2,……省略n個引數……)

再來回顧下 apply() 方法,與 call() 方法類似,但是接受的引數必須是陣列。

格式:Fn.call(this,[ arg1,arg2,……省略n個引數……])

借用建構函式的本質就是利用 call 或者 apply 把父類中通過 this 指定的屬性和方法複製(借用)到子類建立的例項中。因為 this 物件是在執行時基於函式的執行環境繫結的。

也就是說,this 等於window ,而當函式被作為某個物件的方法呼叫時,this 等於那個物件。call、apply 方法可以用來代替另一個物件呼叫一個方法。call、apply 方法可將一個函式的物件上下文從初始的上下文改變為 this 指定的新物件。

總之,在子類函式中,通過 call() 方法呼叫超型別後,子型別的例項可以訪問到子型別和超型別中的所有屬性和方法。這樣就實現了子類向父類的繼承,而且還解決了原型物件上對引用型別值的誤修改操作。

(一) 解決原型鏈繼承的問題(優點)

由於引用型別值共用同一個記憶體空間,子型別繼承超型別的同時,構造出的例項也複製了相同的超型別屬性。改一個例項就等於所有例項發生了改變。

借用建構函式剛好可以解決這個問題。 程式碼如下: ```js // 父建構函式 function Father() { this.child = { name: 'father' } } // 子建構函式 function Children() { Father.call(this) }

// 建立子例項 let c1 = new Children() let c2 = new Children()

// 就算是引用型別值也沒有了問題,太棒了 c1.child.name = "我修改了name" console.log(c1.child)// { name: '我修改了name' }

console.log(c2.child)// { name: 'father' } ``` 從結果中可以看到,更改 c1 例項的屬性,c2 例項不受影響。借用建構函式繼承的例項不共享屬性,相互之間不干擾。

(二) 解決原型鏈傳遞引數的問題(優點)

原型鏈無法正常傳遞引數,可以通過借用建構函式方式解決。

程式碼如下: ```js // 父建構函式 function Father(name, age) { this.age = age this.child = { name: name } } // 子建構函式 function Children() { Father.call(this, 'father', 111) }

// 建立子例項 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' } ``` 從結果中可以看到,無論是值型別還是引用型別值,借用建構函式繼承中傳遞引數,然後修改某個例項,並不會應用到所有例項屬性中,每個例項儲存自己的那份資料,具有很好的隔離性。

(三) 原型屬性和方法無法繼承(缺點)

由於不是原型鏈繼承,僅僅是借用建構函式,那麼無法繼承原型上的屬性和方法。

建構函式中定義的屬性和方法雖然可以訪問,但是每個例項都複製了一份,如果例項過多,方法過多,佔用記憶體就大,那麼方法都在建構函式中定義,函式複用就無從談起。

本來我們就是用 prototype 來解決複用問題。

如果方法都作為了例項自己的方法,當需求改變後,要改變其中的一個方法,那麼之前的所有例項,其中的方法都不能做出更新,只有後面的例項才能訪問到新方法。

程式碼如下: ```js // 父建構函式 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 // 子建構函式 function Children() { Father.call(this) }

// 建立子例項 let c1 = new Children() // 呼叫原型方法,例項訪問不到 c1.alertName() // TypeError: c1.alertName is not a function

// 訪問原型屬性,例項中未定義 console.log(c1.age) // undefined

// 可以訪問例項屬性,但是每個例項都存有自己一份 name 值 console.log(c1.name) // father

// 可以訪問例項方法,但是每個例項都存有自己一份 speakName1() 方法, // 且方法過多,記憶體佔用量大,這就不叫複用了 c1.speakName1()// speakName1

c1.speakName2()// speakName2

c1.speakName3()// speakName3

c1.speakName4()// speakName4

// instanceof isPrototypeOf 無法判斷例項和型別的關係 console.log(Father.prototype.isPrototypeOf(c1))// false console.log(c1 instanceof Father)// false ``` 從結果中可以看出,借用建構函式繼承後,例項無法呼叫原型方法和訪問屬性,但是可以訪問例項屬性和方法。

而且instanceof 和 isPrototypeOf 無法判斷例項和型別的關係。

五、 組合繼承

(一) 解決原型鏈和建構函式繼承的問題(優點)

上述兩者繼承方式,無論是單獨使用原型鏈繼承還是借用建構函式繼承都有自己的侷限性,最好的方式是,將兩者結合一起使用,發揮各自的優勢。

組合繼承就是將原型鏈繼承和借用建構函式繼承組合起來使用。利用原型鏈繼承繼承原型方法,利用借用建構函式繼承繼承例項屬性。這樣既能實現函式複用,又能保證每個例項間的屬性不會相互影響。

程式碼如下: ```js // 父建構函式 function Father(name) { this.child = { name: name } } // 父原型上繫結方法 Father.prototype.alertName = function () { console.log(this.child) } // 子建構函式 借用建構函式繼承父 function Children(name) { Father.call(this, name) } // 原型鏈繼承 Children.prototype = new Father()

Children.prototype.constructor = Children

// 子原型上建立 函式 Children.prototype.speakName = function () { console.log('speakName') } // 建立子例項 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 這行程式碼不明白,飛鴻看前幾遍的時候也是雲裡霧裡,後來明白了。

這一步為建立子型別原型物件新增 constructor 屬性,從而彌補因重寫原型而失去的預設的 constructor 屬性。

(二) 重複建立例項屬性(缺點)

js ……程式碼省略…… // 子建構函式 借用建構函式繼承父 function Children(name) { Father.call(this, name) //第二次呼叫 Father 建構函式 } // 原型鏈繼承 Children.prototype = new Father() //第一次呼叫 Father 建構函式 ……程式碼省略…… 在 new Father() 構造例項時,呼叫了一次 Father 的建構函式建立了一份例項屬性 child,其繫結在原型物件上。

在 new Children('c') 構造例項時,由於原型指向 Father例項,所以又呼叫了一次 Father 的建構函式建立了一份例項屬性 child,其繫結在例項上。

如果例項和原型上屬性同名,那麼例項上的屬性就遮蔽了原型上的。

那麼問題就來了,兩次建立後子型別的原型上和例項物件上都有一份例項屬性。這個不就很影響效能和複用嗎?

六、 原型式繼承

原型式繼承的本質就是用一個物件作為另一個物件的基礎。

有這個物件的話,將它傳給 object,然後再根據具體需求將得到的新物件改一改。 程式碼如下: ```js // 基於已有的物件建立新物件,同時還不用建立自定義型別。 function object(obj) { // 臨時建構函式 function fn() {} // 傳入的物件替換臨時建構函式的原型 fn.prototype = obj // 最後返回臨時建構函式的一個例項 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' ] ``` 從結果中可以看到,經過這個方法的例項物件中的引用型別值和其他例項物件共享,而值型別隔離。實際上創造了兩個 father 物件的副本。

(一) Object.create

ES5 中的 Object.create() 方法規範化了原型式繼承。

Object.create() 方法建立一個新物件,使用現有的物件來提供新建立的物件的 proto

提供兩個入參,第一個是新建立的原型物件;第二個是為新建立的物件新增屬性的物件,同 Object.defineProperties()(看過vue2原始碼一定知道)。

程式碼如下: ```js // 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' ] ```

七、 寄生式繼承

寄生式繼承是基於原型式繼承上修改的,本質上就是建立一個僅用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件,最後再像真的是它做了所有工作一樣返回物件。

缺點就是:使用寄生式繼承來為物件新增函式,會由於不能做到函式複用而降低效率,同借用建構函式繼承的缺點。

程式碼如下: ```js // 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 ```

八、 寄生組合式繼承

寄生組合式繼承本質就是寄生式繼承來繼承超型別的原型,然後再將結果指定給子型別的原型。

通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。

基本思路就是不必為了指定子型別的原型而呼叫超型別的建構函式,我們所需的無非就是超型別的一個副本而已。

寄生組合式繼承是引用型別最理想的繼承正規化。

程式碼如下: ```js / * 實現了寄生組合式繼承的最簡單形式 * @param {} children 子型別 * @param {} father 超型別 */ function inheritPrototype(children, father) { // 建立物件:建立超型別的副本 let prototype = Object.create(father.prototype) // 增強物件:為建立的副本新增constructor屬性,彌補因為重寫原型而失去的預設的constructor的屬性 prototype.constructor = children // 指定物件:將新建立的副本賦值給子型別的原型 children.prototype = prototype }

// 父建構函式 function Father(name) { this.child = { name: name } } // 父原型上繫結方法 Father.prototype.alertName = function () { console.log(this.child) } // 子建構函式 借用建構函式繼承父 function Children(name) { Father.call(this, name) } inheritPrototype(Children, Father)

// 子原型上建立 函式 Children.prototype.speakName = function () { console.log('speakName') } // 建立子例項 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 實現了繼承,原理同寄生組合式繼承。其實就是語法糖。

程式碼如下: ```js // 父類 class Father { // 父建構函式 constructor(name) { this.child = { name: name } } alertName() { console.log(this.child) }

} // 子類繼承父類 class Children extends Father { constructor(name) { // super 表示父類的建構函式,用來新建父類的 this 物件。 // 因為子類沒有自己的this,必須要呼叫super獲取父類的this物件,並加以修改 // 等同於 Father.prototype.constructor.call(this) // 同時綁定了子類的this super(name) } speakName() { // 等同於 Father.prototype.alertName() 綁定了子類 Children 的this super.alertName() } }

// 建立子例項 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 ```

十、 總結

本文首發於公眾號「前端keep」,歡迎關注。 繼承.png

(一) 原型鏈

本質:利用超型別的例項替換子型別的原型物件

優點: 1. 非常純粹的繼承關係,例項是子類的例項,也是父類的例項 2. 父類新增原型方法和原型屬性,子類都能訪問到

缺點: 1. 重寫方法需要在替換原型之後 2. 不能使用物件字面量建立原型方法 3. 無法實現多繼承 4. 引用型別被所有例項共享 5. 建立子類例項時,無法向父類建構函式傳參

(二) 借用建構函式

本質:使用 call() 和 apply() 借用超型別的建構函式來增強子類的例項

優點: 1. 解決了原型鏈中,子類例項共享父類引用型別的問題 2. 建立子類例項時,可以向父類傳遞引數 3. 可以實現多繼承(call多個父類物件)

缺點: 1. 例項並不是父類的例項,只是子類的例項 2. 只能繼承父類的例項屬性和方法,不能繼承原型屬性和原型方法 3. 無法實現函式複用,每個子類都有父類例項函式的副本,影響效能

(三) 組合繼承

本質:將原型鏈和借用建構函式組合

優點: 1. 彌補了原型鏈和借用建構函式的缺陷,可以繼承例項屬性和例項方法,也可以繼承原型屬性和原型方法 2. 既是子類的例項,也是父類的例項 3. 不存在引用型別共享問題 4. 可傳參 5. 函式可複用

缺點: 1. 呼叫了兩次的父類函式,有效能問題 2. 由於兩次呼叫,會造成例項和原型上有相同的屬性或方法

(四) 原型式繼承

本質:借用原型以已有物件為基礎建立新物件

優點: 1. 不需要單獨建立建構函式

缺點: 1. 多個例項共享被繼承的屬性,存在被篡改的情況 2. 不能傳遞引數

(五) 寄生式繼承

本質:建立一個僅用於封裝繼承過程的函式,內部增強物件,最後返回物件

優點: 1. 只需要關注物件本身,不在乎型別和建構函式的場景

缺點: 1. 難以複用函式 2. 無法傳遞引數 3. 多個例項共享被繼承的屬性,存在被篡改的情況

(六) 寄生組合式繼承

本質:寄生式繼承來繼承超型別的原型,然後再將結果指定給子型別的原型

(七) 類extends繼承

ES6使用 class 利用 extends 實現了繼承,原理同寄生組合式繼承。

ES5 和 ES6繼承區別

ES5 的繼承本質就是先創造子類的例項物件 this,然後再將父類的方法新增到 this 上面(Parent.apply(this))。

ES6的繼承本質是先建立父類的例項物件 this(所以必須先呼叫 super 方法),然後在使用子類的建構函式修改 this。

作者不才,文中若有錯誤,望請指正,避免誤人子弟。

十一、 參考文獻

《JavaScript高階程式設計第3版》

《ES6標準入門第3版》

本文首發於公眾號「前端keep」,歡迎關注。

最後,希望大家一定要點贊三連。

更多文章都在我的blog地址

「其他文章」