因爲JS中沒有類的概念,無法像其他語言一樣完成“繼承”,只能通過一些方式進行模擬,即使是ES6中的class也只是一個語法糖,也是通過ES5來完成的繼承。下面介紹JS中的繼承方式。
注:下面用類
來指代構造函數
。
構造函數繼承
通過使用call
函數在子類B中調用父類A的構造函數,將父類的成員變量和成員函數傳給子類
function A(name){
this.name = name
}
A.prototype.GetName=function(){
return this.name
}
function B(){
A.call(this, 'argument')
}
var b = new B()
console.log(b.name) // argument
console.log(b.GetName()) // Uncaught TypeError: b.GetName is not a function
缺點:無法繼承父類的原型上的函數和變量,只能繼承成員屬性和成員變量。
原型鏈繼承
使子類的原型prototype
指向父類的實例對象,這樣子類的實例通過原型鏈訪問到父類的屬性和函數
function A(name){
this.name = name
this.color = ['red', 'green', 'blue']
}
A.prototype.GetName=function(){
return this.name
}
function B(){
}
B.prototype = new A('nameA')
var b1 = new B()
var b2 = new B()
console.log(b1.color) // ['red', 'green', 'blue']
console.log(b2.color) // ['red', 'green', 'blue']
b1.color.push('gray')
console.log(b2.color) // ['red', 'green', 'blue', 'gray']
缺點:因爲子類的原型是父類的實例對象,所以如果父類的實例上有一個引用類型對象,例如上例中的color
,那麼所有的子類的實例的color
都是同一個對象,在任何一個對象中修改該屬性,其他實例中也會被修改。
組合繼承
結合前兩種繼承方式,
// 父類
function A(name) {
this.name = name;
this.color = ['red', 'blue']
this.conFun = function () {
console.log('super type')
}
}
// A原型添加方法
A.prototype.getName = function () {
return this.name
}
// 子類1
function B(name) {
A.call(this, name);
this.fruit = ['apple', 'banana']
}
// 不使用B.prototype = new A(),保證A的成員屬性不會出現在B的原型鏈上
B.prototype = A.prototype
B.prototype.constructor = B
// 這裏B原型添加方法必須在繼承之後,否則,該原型中的方法會因爲prototype指向改變而消失
B.prototype.getBName = function () {
return 'B:'+this.name
}
// 子類2
function C(name) {
A.call(this, name);
this.fruit = ['apple', 'banana']
}
C.prototype = A.prototype
C.prototype.constructor = C
// 這裏C原型添加方法必須在繼承之後,否則,該原型中的方法會因爲prototype指向改變而消失
C.prototype.getCName = function () {
return 'C:'+this.name
}
var b = new B('bbbname')
var c = new C('cccname')
// B上可以訪問getCName,C上可以訪問getBName
b.getCName() // C:bbbname
c.getBName() // B:cccname
缺點:這種繼承方式規避了前面兩種繼承方式的缺點,但是仍然有點問題,這種直接將父類的原型(引用類型)賦值給子類的原型,那麼所有繼承於該父類的子類(可能有多個),他們的原型是同一個對象,多個子類都在原型上添加方法時,這些方法在每一個子類的實例中都可以被訪問到。
原型式繼承
該繼承方式與前面的不太一樣,因爲這種不需要構造函數,僅僅需要對象就可以了。
var superobj = {
name: 'superobj',
color: ['red', 'blue'],
printname: function(){
console.log(this.name)
}
}
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
// 子對象
var subobj1 = object(superobj)
var subobj2 = object(superobj)
subobj1.color.push('yellow')
console.log(subobj2.color) // ['red', 'blue', 'yellow']
缺點:無法複用,無法在原型上添加方法
寄生式繼承
寄生式本質上和原型式沒有什麼區別,只是封裝了一個用來繼承的函數,在這個函數中可以對生成的子對象進行一些定製。
var superobj = {
name: 'superobj',
color: ['red', 'blue'],
printname: function(){
console.log(this.name)
}
}
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
function InheritObj(sobj, custumerobj){
var clone = object(sobj)
Object.assign(clone, custumerobj)
return clone
}
// 子對象
var subobj1 = InheritObj(superobj)
var subobj2 = InheritObj(superobj)
subobj1.color.push('yellow')
console.log(subobj2.color) // ['red', 'blue', 'yellow']
寄生組合繼承
該方式是目前爲止最完美的解決方案,解決了前面出現的問題
這種方式是構造函數繼承+寄生式繼承的結合
function object(sobj){
function Func(){}
Func.prototype = sobj
return new Func()
}
function InheritObject(SubType, SuperType){
var prototype = object(SuperType.prototype)
prototype.constructor = SubType
SubType.prototype = prototype
}
// 父類
function SuperType(){
this.type = 'super'
this.colors = ['red', 'blue', 'yellow']
}
SuperType.prototype.printColors = function(){
console.log(this.colors)
}
// 子類
function SubType(){
// 構造函數繼承
SuperType.call(this)
this.type = 'sub'
}
// 寄生繼承
InheritObject(SubType, SuperType)
SubType.ptoyotype.printtype = function(){
console.log(this.type)
}
var sub1 = new SubType()
var sub2 = new SubType()
sub1.colors.push('green')
sub1.printColors() // ['red', 'blue', 'yellow', 'green']
sub2.printColors() // ['red', 'blue', 'yellow']
利用ES6的方法改寫寄生組合式繼承
利用Object.create
和Object.assign
函數
// 父類
function SuperType(){
this.type = 'super'
this.colors = ['red', 'blue', 'yellow']
}
SuperType.prototype.printColors = function(){
console.log(this.colors)
}
// 子類
function SubType(){
// 構造函數繼承
SuperType.call(this)
// 繼承於多個類的情況
OtherSuperType.call(this)
this.type = 'sub'
}
// 關鍵點,將父類原型包裝一層賦值給子類原型
SubType.prototype = Object.create(SuperType.prototype)
// 混入其他父類的原型屬性
Object.assign(SubType.prototype, OtherSuperType.prototype)
SubType.prototype.constructor = SubType
SubType.ptoyotype.printtype = function(){
console.log(this.type)
}
var sub1 = new SubType()
var sub2 = new SubType()
sub1.colors.push('green')
sub1.printColors() // ['red', 'blue', 'yellow', 'green']
sub2.printColors() // ['red', 'blue', 'yellow']
總結
這些繼承的方式主要出現的是如下三個問題:
- 如何將
實例屬性
和原型上的函數
都繼承(原型鏈繼承
和構造函數繼承
的問題,組合繼承
解決了)。 - 非函數屬性出現在原型鏈上會被所有實例共享,從而出現一個實例修改了,其他實例受影響(原型鏈繼承中,將父類實例作爲子類的原型造成的問題)。
同一個父類
的多個子類原型對象相同
,導致不同子類添加的原型函數會被共享(這是組合繼承
、原型式繼承
和寄生式繼承
的缺點),這是因爲直接將父類的原型賦值給子類的原型。
針對每個問題的關鍵解決方式
原則:構造函數中設置非函數屬性,原型中存儲函數屬性
第一個問題:實例屬性必須通過構造函數繼承實現,即SuperType.call(this)
,即必須通過構造函數+原型組合完成。
第二個問題:不能將父類的實例賦值給子類的原型。
第三個問題:不能將父類的prototype
直接賦值給子類的prototype
,需要在父類prototype
外層套一個對象包裝,再賦值給子類prototype
,這也是寄生組合繼承中InheritObject
函數所做的。