class 是如何實現繼承的?
我相信時至今日,大部分同學看完題目都能很快的寫出答案。
使用 ES 6 提供的類,能夠很快的實現繼承。
class Parent {
constructor() {
this.name = '爸爸';
this.books = ['JAVA']
}
showName() {
console.log(this.name)
}
}
class Child extends Parent {
constructor() {
super();
this.name = '兒子';
this.school = 'high school';
}
showSchool() {
console.log(this.school);
}
}
複製程式碼
但是,其實 JavaScript 本身是沒有類這個概念的,class 只是一個語法糖,Parent 與 Child 本質上還是函式。所以 class 提供的繼承是基於 JavaScript 的原型鏈實現的。那麼如何通過原型鏈實現繼承呢?
ES5中的繼承
JavaScript 中繼承的本質就是子類共享父類的原型物件。
ps:僅個人觀點,有什麼不妥請大家及時反駁。
類式繼承
類式繼承通過子類的原型物件指向父類的例項,完成繼承。
程式碼
function Parent() {
this.name = '爸爸';
this.books = ['JAVA'];
}
Parent.prototype.showName = function() {
console.log(this.name);
}
function Child() {
this.name = '兒子';
this.school = 'high school'
}
Child.prototype = new Parent(); // 子類的原型物件指向父類的例項,子類就能訪問 Parent 原型物件上的方法了
Child.prototype.showSchool = function() {
console.log(this.school);
}
複製程式碼
驗證
const c = new Child();
// 呼叫父類方法
c.showName(); // 兒子
// 呼叫子類方法
c.showSchool(); // high school
複製程式碼
不足
看起來類式繼承很完美的完成了工作,Child 的例項化的物件正常呼叫了父類的方法。但是我們看看 c 到底長什麼樣子。
{
name: '兒子',
school: 'high school',
__proto__: {
books: ['JAVA'],
name: '爸爸',
showSchool: f (),
__proto__: {
showName: f(),
constructor: f Parent()
// ...
}
}
}
複製程式碼
可以看出,類式繼承具有以下幾個問題:
- 父類的屬性只例項化了一次,如果所有例項共享 books 這個屬性,任意一個修改了例項修改了 books 的話,而其他例項不知情,這種情況是很危險的。由於資訊不對稱,很容易出現bug。
- 物件 c 的 proto 沒有 constructor。
- 有兩個name變數,節約記憶體,可以去掉一個。
寄生組合式繼承
- 通過每次呼叫父類,傳入this,使引用型別的值不共享。
- 建立一箇中間物件,顯式的設定 constructor。
- 子類的原型物件指向這個中間物件,中間物件的__proto__指向父類的原型物件。
ps:提個問題,這裡的中間物件其實可以不用建立,直接指定子類的原型物件的__proto__為父類的原型物件(Object.setPrototypeOf)是否可行?文末說下一我的看法。
程式碼
function Parent() {
this.name = '爸爸';
this.books = ['JAVA'];
}
Parent.prototype.showName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this); // 像不像 class 中的 super()
this.name = '兒子';
this.school = 'high school'
}
// Object.create 建立一箇中間物件
// 中間物件的__proto__指向父類的原型物件
// 子類的原型物件指向這個中間物件
Child.prototype = Object.create(Parent.prototype, {
// 顯示的指定 constructor
constructor: {
value: Child,
enumerable: false, // 不可遍歷
writable: true, // 可改寫
configurable: true
}
})
Child.prototype.showSchool = function() {
console.log(this.school);
}
複製程式碼
驗證
const c = new Child();
// 呼叫父類方法
c.showName(); // 兒子
// 呼叫子類方法
c.showSchool(); // high school
複製程式碼
可以看一下 c 的結構
{
name: "兒子",
books: ['JAVA'],
school: "high school",
__proto__: {
showSchool: ƒ (),
constructor: ƒ Child(),
__proto__: {
showName: ƒ (),
constructor: ƒ Parent(),
// ...
}
}
}
複製程式碼
上面提到的問題都得到了解決,有興趣的同學可以去對比一下與class例項化後的繼承的結構是否有區別。
小結
- JavaScript 本身是沒有類的概念,繼承也是通過原型鏈實現的,
- JavaScript 中繼承的本質就是子類共享父類的原型物件。
ps: 上面的問題,我覺得是可行的。但是
由於現代 JavaScript 引擎優化屬性訪問所帶來的特性的關係,更改物件的 [[Prototype]]在各個瀏覽器和 JavaScript 引擎上都是一個很慢的操作。其在更改繼承的效能上的影響是微妙而又廣泛的,這不僅僅限於 obj.proto = ... 語句上的時間花費,而且可能會延伸到任何程式碼,那些可以訪問任何[[Prototype]]已被更改的物件的程式碼。如果你關心效能,你應該避免設定一個物件的 [[Prototype]]。相反,你應該使用 Object.create()來建立帶有你想要的[[Prototype]]的新物件。 ——摘自 MDN
參考資料:
- 【JavaScript 設計模式】 —— 張容銘
- MDN