本文將從最簡單的例子開始,從零講解在 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.prototypeCat.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 中的 this
- 理性分析 JavaScript 中的原型
參考書籍
- 《JavaScript高級程序設計(第3版)》
- 《Java核心技術 卷Ⅰ(第9版)》