本文將從最簡單的例子開始,從零講解在 JavaScript 中如何實現繼承。
小例子
現在有個需求,需要實現 Cat 繼承 Animal ,建構函式如下:
function Animal(name){
this.name = name
}
function Cat(name){
this.name = name
}
複製程式碼
注:如對繼承相關的 prototype、constructor、__proto__、new 等內容不太熟悉,可以先檢視這篇文章:理性分析 JavaScript 中的原型
繼承
在實現這個需求之前,我們先談談繼承的意義。繼承本質上為了提高程式碼的複用性。
對於 JavaScript 來說,繼承有兩個要點:
- 複用父建構函式中的程式碼
- 複用父原型中的程式碼
下面的內容將圍繞這兩個要點展開。
第一版程式碼
複用父建構函式中的程式碼,我們可以考慮呼叫父建構函式並將 this 繫結到子建構函式。
複用父原型中的程式碼,我們只需改變原型鏈即可。將子建構函式的原型物件的 __proto__ 屬性指向父建構函式的原型物件。
第一版程式碼如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
Cat.prototype.__proto__ = Animal.prototype
複製程式碼
檢驗一下是否繼承成功:我們在 Animal 的原型物件上新增 eat 函式。使用 Cat 建構函式生成一個名為 'Tom' 的例項物件 cat 。程式碼如下:
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
複製程式碼
經檢驗,成功複用父建構函式中的程式碼,並複用父原型物件中的程式碼,原型鏈正常。
圖示
弊端
__proto__ 屬性雖然可以很方便地改變原型鏈,但是 __proto__ 直到 ES6 才新增到規範中,存在相容性問題,並且直接使用 __proto__ 來改變原型鏈非常消耗效能。所以 __proto__ 屬性來實現繼承並不可取。
第二版程式碼
針對 __proto__ 屬性的弊端,我們考慮使用 new 操作符來替代直接使用 __proto__ 屬性來改變原型鏈。
我們知道例項物件中的 __proto__ 屬性指向建構函式的 prototype 屬性的。這樣我們 Animal 的例項物件賦值給 Cat.prototype 。不就也實現了Cat.prototype.__proto__ = Animal.prototype
語句的功能了嗎?
程式碼如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
複製程式碼
使用這套方案有個問題,就是在將例項物件賦值給 Cat.prototype 的時候,將 Cat.prototype 原有的 constructor 屬性覆蓋了。例項物件的 constructor 屬性向上查詢得到的是建構函式 Animal 。所以我們需要矯正一下 Cat.prototype 的 constructor 屬性,將其設定為建構函式 Cat 。
圖示
優點
相容性比較好,並且實現較為簡單。
弊端
使用 new 操作符帶來的弊端是,執行 new 操作符的時候,會執行一次建構函式將建構函式中的屬性繫結到這個例項物件。這樣就多執行了一次建構函式,將原本屬於 Animal 例項物件的屬性混到 prototype 中了。
第三版程式碼
考慮到第二版的弊端,我們使用一個空建構函式來作為中介函式,這樣就不會將建構函式中的屬性混到 prototype 中,並且減少了多執行一次建構函式帶來的效能損耗。
程式碼如下:
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
複製程式碼
圖示
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
複製程式碼
相關知識點
參考書籍
- 《JavaScript高階程式設計(第3版)》
- 《Java核心技術 卷Ⅰ(第9版)》