手把手教你如何實現繼承
本文將從最簡單的例子開始,從零講解在 JavaScript 中如何實現繼承。
小例子
現在有個需求,需要實現 Cat 繼承 Animal ,建構函式如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(244, 245, 246);">function Animal(name){ this.name = name } function Cat(name){ this.name = name }</pre>
繼承
在實現這個需求之前,我們先談談繼承的意義。繼承本質上為了提高程式碼的複用性。
對於 JavaScript 來說,繼承有兩個要點:
-
複用父建構函式中的程式碼
-
複用父原型中的程式碼
下面的內容將圍繞這兩個要點展開。
第一版程式碼
複用父建構函式中的程式碼,我們可以考慮呼叫父建構函式並將 this 繫結到子建構函式。
複用父原型中的程式碼,我們只需改變原型鏈即可。將子建構函式的原型物件的 proto 屬性指向父建構函式的原型物件。
第一版程式碼如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(244, 245, 246);">function Animal(name){ this.name = name } function Cat(name){ Animal.call(this,name) } Cat.prototype.__proto__ = Animal.prototype</pre>
檢驗一下是否繼承成功:我們在 Animal 的原型物件上新增 eat 函式。使用 Cat 建構函式生成一個名為 'Tom' 的例項物件 cat 。程式碼如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(244, 245, 246);">function Animal(name){ this.name = name } function Cat(name){ Animal.call(this,name) } Cat.prototype.__proto__ = Animal.prototype // 新增 eat 函式 Animal.prototype.eat = function(){ console.log('eat') } var cat = new Cat('Tom') // 檢視 name 屬性是否成功掛載到 cat 物件上 console.log(cat.name) // Tom // 檢視是否能訪問到 eat 函式 cat.eat() // eat // 檢視 Animal.prototype 是否位於原型鏈上 console.log(cat instanceof Animal) // true // 檢視 Cat.prototype 是否位於原型鏈上 console.log(cat instanceof Cat) //true</pre>
經檢驗,成功複用父建構函式中的程式碼,並複用父原型物件中的程式碼,原型鏈正常。
圖示

手把手教你如何實現繼承
弊端
proto屬性雖然可以很方便地改變原型鏈,但是 proto 直到 ES6 才新增到規範中,存在相容性問題,並且直接使用 proto 來改變原型鏈非常消耗效能。所以 proto 屬性來實現繼承並不可取。
第二版程式碼
針對 proto 屬性的弊端,我們考慮使用 new 操作符來替代直接使用 proto 屬性來改變原型鏈。
我們知道例項物件中的 proto 屬性指向建構函式的 prototype 屬性的。這樣我們 Animal 的例項物件賦值給 Cat.prototype 。不就也實現了Cat.prototype. proto = Animal.prototype 語句的功能了嗎?
程式碼如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(244, 245, 246);">function Animal(name){ this.name = name } function Cat(name){ Animal.call(this,name) } Cat.prototype = new Animal() Cat.prototype.constructor = Cat</pre>
使用這套方案有個問題,就是在將例項物件賦值給 Cat.prototype 的時候,將 Cat.prototype 原有的 constructor 屬性覆蓋了。例項物件的 constructor 屬性向上查詢得到的是建構函式 Animal 。所以我們需要矯正一下 Cat.prototype 的 constructor 屬性,將其設定為建構函式 Cat 。
圖示

手把手教你如何實現繼承
優點
相容性比較好,並且實現較為簡單。
弊端
使用 new 操作符帶來的弊端是,執行 new 操作符的時候,會執行一次建構函式將建構函式中的屬性繫結到這個例項物件。這樣就多執行了一次建構函式,將原本屬於 Animal 例項物件的屬性混到 prototype 中了。
第三版程式碼
考慮到第二版的弊端,我們使用一個空建構函式來作為中介函式,這樣就不會將建構函式中的屬性混到 prototype 中,並且減少了多執行一次建構函式帶來的效能損耗。
程式碼如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(244, 245, 246);">function Animal(name){ this.name = name } function Cat(name){ Animal.call(this,name) } function Func(){} Func.prototype = Animal.prototype Cat.prototype = new Func() Cat.prototype.constructor = Cat</pre>
圖示

手把手教你如何實現繼承
ES6
使用 ES6 就方便多了。可以使用 extends 關鍵字實現繼承, 複用父原型中的程式碼。使用 super 關鍵字來複用父建構函式中的程式碼。
程式碼如下:
class Animal { constructor(name){ this.name = name } eat(){ console.log('eat') } } class Cat extends Animal{ constructor(name){ super(name) } } let cat = new Cat('Tom') console.log(cat.name) // Tom cat.eat() // eat
“我自己是一名從事了6年前端的老程式設計師,今年年初我花了一個月整理了一份最適合2019年學習的web前端乾貨,從最基礎的HTML+CSS+JS到移動端HTML5到各種框架都有整理,送給每一位前端小夥伴,web前端學習交流,“550389714”這裡是小白聚集地,歡迎初學和進階中的小夥伴。"
