学习原型链(二)继承

200 阅读5分钟

直接利用原型链继承

这是最简单粗暴的继承方式。让构造函数 A 的原型对象是构造函数 B 的实例,即 A.prototype = new B()。这样一来,A 的实例的原型就会指向 B 的实例,A 通过原型链就能访问到 B 的属性和方法。假设 B 的原型又是 C 的实例,即 B 继承了 C,沿着原型链一直向上,就构成了实例与原型的链条,也实现了继承。

function SuperType () {
  this.property = true;
}

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

function SubType () {
  this.subproperty = false;
}

/* 继承 SuperType */
SubType.prototype = new SuperType();

SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function () {
  return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());  /* true */

上面的例子中,getSuperValue 定义在 SuperType 的原型中,而 property 定义在了 SuperType 的实例中。随着继承关系的实现, SuperType 的实例成为了 SubType 的原型,property 也就定义在了 SubType 的原型中了。随后,我们将 SubType 原型的 constructor 修改为 Subtype,并在其中定义了 getSubValue 方法。SubType 的构造函数还在其实例中定义了 subproperty 属性。

当我们通过 instance 实例访问 getSuperValue 函数时,查找顺序为:instance -> Subtype.prototype(也是SuperType的实例) -> SubType.prototype.__proto__(即SuperType.prototype),最终在 SuperType 的原型中找到了这个函数并调用。

这种方式存在与「利用原型模式创建对象」类似的问题:构造函数 SuperType 的实例成为 SubType 的原型,那么 SubType 的原型中存在 SuperType 的实例属性,这些属性将会被 SubType 的实例共享。

另一个问题是,无法向 SuperType 传递初始化参数。

借用构造函数

也叫伪造对象或经典继承,即在子类的构造函数中调用父类的构造函数。究其根源,也就是将父类构造函数定义属性和函数的操作由子类构造函数完成,原本父类构造函数会在父类实例中定义这些属性和函数,现在子类也在自己的实例中定义了这些属性和函数。别忘了,作用域链上,子类会屏蔽对父类的同名属性和函数的访问。

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

function SubType (name) {
  /* 继承了 SuperType */
  SuperType.call(this, name);
  
  this.age = 27;
}

var instance1 = new SubType('Nocholas');
instance1.colors.push('black'); /* colors 是借用 SuperType() 定义在 instance1 上的实例属性 */
console.log(instance1.colors);  /* 'red, blue, green, black' */

var instance2 = new SubType('Grey');
console.log(instance2.colors);  /* 'red, blue, green' */
console.log(instance2.name);    /* 'Grey' */
console.log(instance2.age);     /* 27 */

如果仅仅是借用构造函数,那么也就无法避免构造函数模式存在的问题 —— 函数都在构造函数中定义,无法复用。

组合继承

组合继承 = 原型链 + 借用构造函数

  • 原型链:继承父类的原型的属性和函数
  • 借用构造函数:将父类构造函数定义的属性在子类实例中定义
function SuperType (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Super.prototype.sayName = function () {
  console.log(this.name);
}

function SubType (name, age) {
  /* 继承 SuperType 属性 */
  SuperType.call(this, name);
  
  this.age = age;
}

/* 继承 SuperType 方法 */
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  console.log(this.age);
}

我们利用 SuperType.call(this, name)SuperType 的属性定义到了 SubType 的实例中,也就不存在共享属性的问题了;又利用 SubType.prototype = new SuperType()SubType 提供了 SuperType 原型中的函数。这样一来,SuperType 的属性和函数都得到了继承,且属性不被共享、函数是共享的。

有一个小细节值得留意,由于 SubType.prototypeSuperType 的实例,所以也会存在 namecolors 属性,这是我们使用原型链模式带来的附属产品。只不过由于 SubType 实例中存在同名属性,「屏蔽」了对 SubType.prototype.nameSubType.prototype.age 的访问而已。后面我们还会再优化它。

原型式继承

准确的讲,我认为「原型式继承」只是新建了一个原型指向传入对象的空白对象。

function object (o) {
  function F () {};
  F.prototype = o;
  return new F();
}

这种继承方法虽然不会作为直接可以使用的继承方式,但可以作为其他继承方式的一种工具。千万不要忘记,o 作为原型,其属性是被共享的。

ES5 中规范化了原型式继承 —— Object.create() 方法。它接收两个参数,第一个参数与 object(o) 的参数相同;第二个参数可选,可为返回对象定义新的属性。

寄生式继承

寄生式继承的思路与寄生构造函数和工厂函数类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像是真地是它做了所有工作一样返回对象。

function createAnother (original) {
  var clone = object(original);  /* 通过调用函数创建一个新对象 */
  clone.sayHi = function () {    /* 以某种方式来增强这个对象 */
    console.log('Hi');
  }
  return clone;                  /* 返回这个对象 */
}

var person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi();  /* 'Hi' */

anotherPersonperson 为原型,具有 person 的所有属性和函数,而且在实例上定义了自己的 sayHi 函数。

仅适用于不考虑自定义类型和构造函数的情况下可以使用这种模式。但由于不能做到函数复用,所以还不是最完美的方式。

寄生组合式继承

前面提到的组合继承已经是相对比较好的继承方式了。但它调用了两次 SuperType 构造函数,使得 SubType 原型和实例中拥有 namecolors 属性的两份拷贝。

我们改进的思路是:

不必为了指定 SubType 的原型而调用 SuperType 的构造函数,我们需要的无非就是 SuperType 原型的一个副本而已。

我们使用寄生式继承来继承 SuperType 的原型。

function inheritPrototype (subType, superType) {
  var prototype = object(superType.prototype);  /* 创建对象 */
  prototype.constructor = subType;              /* 增强对象 */
  subType.prototype = prototype;                /* 指定对象 */
}


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

SuperType.prototype.sayName = function () {
  console.log(this.name);
}

function SubType (name, age) {
  SuperType.call(this, name);
  
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
  console.log(this.age);
}

使用寄生组合式继承,我们既实现了原型链,又避免了组合继承的两个调用构造函数的问题。