這篇文章将會介紹在 JavaScript 中經常使用的六種繼承方式
1.1 原型繼承
方法:将子類的原型指向父類的執行個體
原理:子類在通路屬性或調用方法時,往上查找原型鍊,能夠找到父類的屬性和方法
function SuperType(name, info) {
// 執行個體屬性(基本類型)
this.name = name || 'Super'
// 執行個體屬性(引用類型)
this.info = info || ['Super']
// 執行個體方法
this.getName = function() { return this.name }
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info }
// 原型繼承
function ChildType(message) { this.message = message }
ChildType.prototype = new SuperType('Child', ['Child'])
// 在調用子類構造函數時,無法向父類構造函數傳遞參數
var child = new ChildType('Hello')
// 子類執行個體可以通路父類的執行個體方法和原型方法
console.log(child.getName()) // Child
console.log(child.getInfo()) // ["Child"]
// 所有子類執行個體共享父類的引用屬性
var other = new ChildType('Hi')
other.info.push('Temp')
console.log(other.info) // ["Child", "Temp"]
console.log(child.info) // ["Child", "Temp"]
- 缺點:在調用子類構造函數時,無法向父類構造函數傳遞參數
- 優點:子類執行個體可以通路父類的執行個體方法和原型方法
- 缺點:所有子類執行個體共享父類的引用屬性
1.2 構造繼承
方法:在子類的構造函數調用父類的構造函數,并将
this
指向子類執行個體
原理:在構造子類時,調用父類的構造函數初始化子類的屬性和方法
function SuperType(name, info) {
// 執行個體屬性(基本類型)
this.name = name || 'Super'
// 執行個體屬性(引用類型)
this.info = info || ['Super']
// 執行個體方法
this.getName = function() { return this.name }
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info }
// 構造繼承
function ChildType(name, info, message) {
SuperType.call(this, name, info)
this.message = message
}
// 在調用子類構造函數時,可以向父類構造函數傳遞參數
var child = new ChildType('Child', ['Child'], 'Hello')
// 子類執行個體可以通路父類的執行個體方法,但是不能通路父類的原型方法
console.log(child.getName()) // Child
console.log(child.getInfo()) // Uncaught TypeError
// 每個子類執行個體的屬性獨立存在
var other = new ChildType('Child', ['Child'], 'Hi')
other.info.push('Temp')
console.log(other.info) // ["Child", "Temp"]
console.log(child.info) // ["Child"]
- 優點:在調用子類構造函數時,可以向父類構造函數傳遞參數
- 缺點:子類執行個體可以通路父類的執行個體方法,但是不能通路父類的原型方法,是以無法做到函數複用
- 優點:每個子類執行個體的屬性獨立存在
1.3 組合繼承
方法:同時使用原型繼承和構造繼承,綜合兩者的優勢所在
原理:通過原型繼承實作原型屬性和原型方法的繼承,通過構造繼承實作執行個體屬性和執行個體方法的繼承
function SuperType(name, info) {
// 執行個體屬性(基本類型)
this.name = name || 'Super'
// 執行個體屬性(引用類型)
this.info = info || ['Super']
// 執行個體方法
this.getName = function() { return this.name }
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info }
// 組合繼承
function ChildType(name, info, message) {
SuperType.call(this, name, info)
this.message = message
}
ChildType.prototype = new SuperType()
ChildType.prototype.constructor = ChildType
// 在調用子類構造函數時,可以向父類構造函數傳遞參數
var child = new ChildType('Child', ['Child'], 'Hello')
// 子類執行個體可以通路父類的執行個體方法和原型方法
console.log(child.getName()) // Child
console.log(child.getInfo()) // ["Child"]
// 每個子類執行個體的屬性獨立存在
var other = new ChildType('Child', ['Child'], 'Hi')
other.info.push('Temp')
console.log(other.info) // ["Child", "Temp"]
console.log(child.info) // ["Child"]
- 優點:在調用子類構造函數時,可以向父類構造函數傳遞參數
- 優點:子類執行個體可以通路父類的執行個體方法和原型方法
- 優點:每個子類執行個體的屬性獨立存在
- 缺點:在實作組合繼承時,需要調用兩次父類構造函數
2.1 原型式繼承
方法:實作一個函數,傳入已有對象,在函數内部将新對象的原型指向原有對象,最後傳回新對象
原理:傳回的新對象繼承原有對象,然後根據需求對得到的對象加以修改即可
var superObject = {
name: 'Super',
info: ['Super'],
getName: function() { return this.name }
}
// 原型式繼承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
// 建立子類執行個體必須基于一個已有對象
var childObject = object(superObject)
// 根據需求對得到的對象加以修改
childObject.message = 'Hello'
// 新建立的執行個體可以通路已有對象的執行個體屬性和執行個體方法
console.log(childObject.name) // Super
console.log(childObject.getName()) // Super
// 所有新建立的執行個體共享已有對象的引用屬性
var otherObject = object(superObject)
otherObject.info.push('Temp')
console.log(otherObject.info) // ["Child", "Temp"]
console.log(childObject.info) // ["Child", "Temp"]
- 要求:建立子類執行個體必須基于一個已有對象
- 缺點:所有新建立的執行個體都會重新定義已有對象的執行個體方法,是以無法做到函數複用
- 缺點:所有新建立的執行個體共享已有對象的引用屬性
2.2 寄生式繼承
方法:建立一個用于封裝繼承過程的函數,在函數内部以某種方式增強對象,且最後傳回對象
原理:基于原型式繼承,類似于工廠模式,将增強對象的過程封裝到一個函數中
var superObject = {
name: 'Super',
info: ['Super'],
getName: function() { return this.name }
}
// 寄生式繼承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function objectFactory(o) {
var clone = object(o) // 建立對象
clone.message = 'Hello' // 增強對象
return clone // 傳回對象
}
// 建立子類執行個體必須基于一個已有對象
var childObject = objectFactory(superObject)
// 新建立的執行個體可以通路已有對象的執行個體屬性和執行個體方法
console.log(childObject.name) // Super
console.log(childObject.getName()) // Super
// 所有新建立的執行個體共享已有對象的引用屬性
var otherObject = object(superObject)
otherObject.info.push('Temp')
console.log(otherObject.info) // ["Child", "Temp"]
console.log(childObject.info) // ["Child", "Temp"]
- 要求:建立子類執行個體必須基于一個已有對象
- 缺點:所有新建立的執行個體都會重新定義已有對象的執行個體方法,是以無法做到函數複用
- 缺點:所有新建立的執行個體共享已有對象的引用屬性
3 寄生式組合繼承
方法:借用寄生式繼承的思路,結合組合繼承的方法,解決組合繼承中需要調用兩次父類構造函數的問題
原理:通過構造繼承實作執行個體屬性和執行個體方法的繼承,通過寄生式繼承實作原型屬性和原型方法的繼承
不用為了指定子類的原型而調用父類的構造函數,而是使用寄生式繼承來繼承父類的原型,然後指定給子類的原型
function SuperType(name, info) {
// 執行個體屬性(基本類型)
this.name = name || 'Super'
// 執行個體屬性(引用類型)
this.info = info || ['Super']
// 執行個體方法
this.getName = function() { return this.name }
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info }
// 寄生式組合繼承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function objectFactory(childType, superType) {
var prototype = object(superType.prototype) // 建立對象
prototype.constructor = childType // 增強對象
childType.prototype = prototype // 将父類原型指定給子類原型
}
function ChildType(name, info, message) {
SuperType.call(this, name, info)
this.message = message
}
objectFactory(ChildType, SuperType)
寄生式組合繼承是 JavaScript 中最常用的繼承方式,ES6 中新增的 extends 底層也是基于寄生式組合繼承的
【 閱讀更多 JavaScript 系列文章,請看 JavaScript學習筆記 】