前端Javascript繼承方式總結

因爲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.createObject.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']

總結

這些繼承的方式主要出現的是如下三個問題:

  1. 如何將實例屬性原型上的函數都繼承(原型鏈繼承構造函數繼承的問題,組合繼承解決了)。
  2. 非函數屬性出現在原型鏈上會被所有實例共享,從而出現一個實例修改了,其他實例受影響(原型鏈繼承中,將父類實例作爲子類的原型造成的問題)。
  3. 同一個父類的多個子類原型對象相同,導致不同子類添加的原型函數會被共享(這是組合繼承原型式繼承寄生式繼承的缺點),這是因爲直接將父類的原型賦值給子類的原型。

針對每個問題的關鍵解決方式

原則:構造函數中設置非函數屬性,原型中存儲函數屬性

第一個問題:實例屬性必須通過構造函數繼承實現,即SuperType.call(this),即必須通過構造函數+原型組合完成。

第二個問題:不能將父類的實例賦值給子類的原型。

第三個問題:不能將父類的prototype直接賦值給子類的prototype,需要在父類prototype外層套一個對象包裝,再賦值給子類prototype,這也是寄生組合繼承中InheritObject函數所做的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章