這篇文章稱為筆記更為合適一些,內容來源於 《JavaScript高階程式設計 (第三版)》第六章 6.3 繼承
JavaScript的幾種繼承方式
- 原型鏈繼承
- 藉助建構函式繼承(經典繼承)
- 組合繼承:原型鏈 + 借用建構函式(最常用)
- 原型式繼承 (Object.create)
- 寄生式繼承
寄生組合式繼承(最理想)
- ES6中的繼承
1. 原型鏈繼承
子型別的原型為父型別的一個例項物件
function Parent() {
this.name = 'bigStar';
this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child() {
this.subName = 'litterStar';
}
// 核心程式碼: 子型別的原型為父型別的一個例項物件
Child.prototype = new Parent();
let child1 = new Child();
let child2 = new Child();
child1.getName(); // bigStar
child1.colors.push('pink');
// 修改 child1.colors 會影響 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
注意核心程式碼: Child.prototype = new Parent();
特點:
- 父類新增在建構函式上面的方法,子類都能訪問到
缺點:
- 來自原型物件的所有屬性被所有例項共享,child1修改 colors 會影響child2的 colors
- 建立子類例項時,無法向父類的建構函式傳參
2.藉助建構函式繼承(經典繼承)
在子類的建構函式中使用 call()或者 apply() 呼叫父型別建構函式
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name, age) {
// 核心程式碼:“借調”父型別的建構函式
Parent.call(this, name);
this.age = age;
}
let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
console.log(child1.name); // litterStar
console.log(child2.name); // luckyStar
// 這種方式只是實現部分的繼承,如果父類的原型還有方法和屬性,子類是拿不到這些方法和屬性的。
child1.getName(); // TypeError: child1.getName is not a function
注意核心程式碼: Parent.call(this, name);
特點:
- 避免引用型別的屬性被所有例項共享
- 建立子類例項時,可以向父類傳遞引數
缺點
- 例項並不是父類的例項,只是子類的例項
- 只能繼承父類的例項屬性和方法,不能繼承原型屬性和方法
- 無法實現函式複用,每次建立例項都會建立一遍方法,影響效能
3.組合繼承:原型鏈 + 借用建構函式(最常用)
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name, age) {
// 核心程式碼①
Parent.call(this, name);
this.age = age;
}
// 核心程式碼②: 子型別的原型為父型別的一個例項物件
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 可以通過子類給父類的建構函式傳參
let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
child1.getName(); // litterStar
child2.getName(); // luckyStar
child1.colors.push('pink');
// 修改 child1.colors 不會影響 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow' ]
注意核心程式碼:Parent.call(this, name);
和Child.prototype = new Parent();
特點
融合了原型鏈繼承和借用建構函式的優點,稱為JavaScript中最常用的繼承模式。
缺點
呼叫了兩次父類建構函式,生成了兩份例項
- 一次是設定子型別例項的原型的時候
Child.prototype = new Parent();
- 一次是建立子型別例項的時候
let child1 = new Child('litterStar');
, 呼叫 new 會執行Parent.call(this, name);
,此時會再次呼叫一次Parent
建構函式
- 一次是設定子型別例項的原型的時候
4. 原型式繼承 (Object.create)
藉助原型可以基於現有方法來建立物件,var B = Object.create(A) 以A物件為原型,生成A物件,B繼承了A的所有屬性和方法。
const person = {
name: 'star',
colors: ['red', 'blue'],
}
// 核心程式碼:Object.create
const person1 = Object.create(person);
const person2= Object.create(person);
person1.name = 'litterstar';
person2.name = 'luckystar';
person1.colors.push('yellow');
console.log(person1.colors); // [ 'red', 'blue', 'yellow' ]
console.log(person2.colors); // [ 'red', 'blue', 'yellow' ]
注意核心程式碼: const person1 = Object.create(person);
特點
沒有嚴格意義上的建構函式,藉助原型可以基於已有物件建立新物件
缺點
- 來自原型物件的所有屬性被所有例項共享,person1修改 colors 會影響person2的 colors,這點跟原型鏈繼承一樣。
5. 寄生式繼承
建立一個用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件
function createObj (original) {
// 通過呼叫函式創新一個新物件
var clone = Object.create(original);
// 以某種方式來增強這個物件
clone.sayName = function () {
console.log('hi');
}
// 返回這個物件
return clone;
}
缺點: 每次建立物件都會建立一遍方法,跟藉助建構函式模式一樣
6.寄生組合式繼承(最理想的)
我們可以先回憶一下JavaScript最常用的繼承模式: 組合繼承(原型鏈 + 借用建構函式),它的最大缺點是會呼叫兩次父建構函式(Child.prototype = new Parent();
和 let child1 = new Child('litterStar');
)。
我們是否可以想辦法是呼叫一次?可以讓 Child.prototype 訪問到 Parent.prototype。
我們不能直接使用 Child.prototype = Parent.prototype
來實現,因為會出現一些副作用,你可能在修改 Child.prototype
的時候會修改Parent.prototype
。
可以使用 Object.create(...)
來實現
Object.create
MDN上的解釋:它會建立一個新物件,使用現有的物件來提供新建立的物件的__proto__
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name, age) {
// 核心程式碼①
Parent.call(this, name);
this.age = age;
}
// 核心程式碼②
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
注意核心程式碼:Parent.call(this, name);
和Child.prototype = Object.create(Parent.prototype);
寄生組合式繼承,集寄生式繼承和組合式繼承的優點,是引用型別最理想的繼承正規化。
7. ES6 中class的繼承
ES6中引入了class關鍵字,可以通過extends關鍵字實現繼承。
class Parent {}
class Child extends Parent {
constructor(name, age, color) {
// 呼叫父類的constructor(name, age)
super(name, age);
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 呼叫父類的toString()
}
}
class關鍵字只是原型的語法糖,JavaScript繼承仍然是基於原型實現的。