前端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函数所做的。

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