JavaScript 继承

125 阅读4分钟

继承

这里是 JavaScript 中比较有趣的一个部分了,首先要理清楚 对象、构造函数、原型对象 这几个概念和它们之间的关系,具体可以参考前面的 02.创建对象。首先明确一点:JS无法实现接口集成,只支持实现继承。接下里我们讲讲如何实现继承。

原型链

理解起来不难。首先我们知道 构造函数有一个原型对象,而实例包含一个指向构造函数原型对象的指针,但这里我们只涉及到了一个原型对象,是不足以形成 的, 的表面意思是 一个原型对象紧接着链接另一个原型对象,而照目前我们的知识来看,构造函数的原型对象只包含 constructor指针和一些定义在原型上的属性和方法,这里面没办法找到另一个原型,那么如果 一个构造函数的原型对象是另一个构造函数的实例呢? ,这样,实例的内部属性指向了构造函数的原型对象,而该原型对象是另一个构造函数的实例,则该原型对象有一个指向了另一个构造函数的原型对象的属性,这样我们就将两个原型对象串成了链。以上还是只有两个原型对象,如果另一个构造函数的原型对象又是其他构造函数的实例呢?层层递进的情况下,我们得到了一条 由原型对象组成的链条原型链

function SuperType () {
  this.isSuperType = true
}

SuperType.prototype.getSuperValue = function () {
  return this.isSuperType
}

function SubType () {
  this.isSubType = true
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.isSubType
} 

let instance = new SubType()

以上代码即实现了继承,instance 实例也获得了一条原型链

  1. SuperType 有一个原型对象,这个原型对象长这样:
{
  constructor: SuperType,
  getSuperValue: (function)
}
  1. 我们使用 new SuperType() 创建了一个实例,该实例自然有一个属性 [[__proto_]] 指向了上述原型对象,同时我们将 SubType 的原型对象设置为该实例,此时,SubType 的原型对象长这样:
{
  isSuperType: true,
  __proto__: {
    constructor: SuperType,
    getSuperValue: (function)
  }
}
  1. 我们为SubType赋值完原型对象之后,我们还在其原型上添加了子类自定义的方法 getSubValue(),此时,SubType 的原型对象就长成了这样:
{
  isSuperType: true,
  getSubType: (function),
  __proto__: {
    constructor: SuperType,
    getSuperValue: (function)
  }
}
  1. 我们创建了SubType 的实例 instance,自然,instance 有一个指向了上述原型对象的属性,那么此时,instance 对象长这样:
instance = {
  isSubType: true,
  __proto__: {
    isSuperType: true,
    getSubType: (function),
    __proto__: {
      constructor: SuperType,
      getSuperValue: (function),
      __proto__: {
        constructor: Object,
        hasOwnProperty: (function),
        isPrototypeOf: (function),
        propertyIsEnumerable: (function),
        toLocaleString: (function),
        toString: (function),
        valueOf: (function)
      }
    }
  }
}

这样我们就值观地看见,instance的表示中存在了两个 __proto__ 属性,我们将这样产生的链就叫做原型链了,然后当我们使用instance的一些属性和方法时,会依据这条链去寻找同名的属性来返回:

instance.isSubType // true
instance.isSuperType // 位于第一层 __proto__ 中,返回true
instance.getSubType() // 位于第一层 __proto__ 中,返回函数运行结果
instance.getSuperValue() // 位于第二层 __proto__ 中,返回函数运行结果

至此我们就完成了:继承,同时为子类添加自定义的方法。
但是原型链继承仍然有缺点:由于所有子类实例都指向了同一个原型对象,而该原型对象时父类的实例,那么该对象的实例的属性被共享了,即出现了:

let instance1 = new SubType()
instance1.isSuperType === instance.isSuperType

这种情况在上一小节中我们也遇到了,显然这样是不太符合我们的期望的,而解决这个问题的办法也和上一小节有些类似。

而我们在操作原型的过程中一定要注意保持住构造函数和原型对象之间的联系,一旦切断了联系,后续创建的实例的 __proto__ 和先前创建的实例的 __proto__ 将不再指向同一个原型对象。这个我们在讲原型的时候也提到过。

组合继承

上面讲到如何利用原型链的方式实现继承,同时指出该方法存在了缺点,即子类指向的原型对象中的 属性 是实例共享的。为了解决这个问题我们可以利用属性覆盖解决。

function SuperType (name) {
  this.name = name
  this.colors = ['red', 'green', 'blue']
}

SuperType.prototype.sayName = function () {
  alert(this.name)
}

function SubType (name, age) {
  // 在这里我们继承属性,使得 实例上的属性覆盖掉原型对象上的属性
  SuperType.call(this, name)
  this.age = age
}

SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
  alert(this.age)
}

let instance1 = new SubType('Nicholad', 21)
let instance2 = new SubType('Bob', 22)
instance1.colors.push('black') // ['red', 'green', 'blue', 'black']
alert(instance2.colors) // ['red', 'green', 'blue']

这个方法很好地覆盖了原型属性,从而达到实例独立,是较为实用的方法。但是该方法仍然存在缺点:我们实例化一次子类会调用两次父类,第一次为覆盖圆形属性时,第二次为更改子类原型属性时。

持续更新在github