JavaScript(五)继承总结

130 阅读8分钟

ES 中只支持实现继承,无法实现接口继承,而且其实现继承主要是依靠原型链来实现的。

1. 原型链继承

  • 利用原型让一个引用类型继承另一个引用类型的属性和方法。

  1. 别忘记默认的原型

  2. 确定原型和实例的关系

  • instanceof
    • console.log(instance instanceof Object);// true
    • 由于原型链的关系,我们可以说 instance 是 Object、SuperType 或 SubType 中任何一个类型的实例。
  • isPrototypeOf( )
    • console.log(Object.prototype.isPrototypeOf(instance)); // true
  1. 谨慎定义方法
  • 给原型添加方法的代码一定要放在替换原型的语句之后。
  • 再通过原型链实现继承时,不能使用对象字面量创建原型方法。
  1. 原型链的问题
  • 引用类型值的原型会被所有实例共享。原先的实例属性成为现在的原型属性。
function SuperType(){
  this.colors = {"red","blue","green"};
}
function SubType(){
}
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);    // "red","blue","green","black"

var instance2 = new SubType();
console.log(instance2.color);   // "red","blue","green","black"
  • 再创建子类型时,没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
  • 原型链的问题是对象实例共享所有继承的属性和方法,实际很少单独使用原型链。

2. 借用构造函数

  • 借用构造函数:也叫伪造对象或经典继承
    • 基本思想:在子类型构造函数的内部调用超类型构造函数。
function SuperType(){
  this.colors = ["red","blue","green"];
}
function SubType(){
  SuperType.call(this);
}

var instaance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);   // "red","blue","green","black"

var instance2 = new SubType();
console.log(instance2.color);   // "red","blue","green"
  1. 传递参数
function SuperType(name){
  this.name = name;
}
function SubType(){
  SuperType.call(this,"qqq946");
  this.age = 17;
}
var instance = new SubType();
console.log(instance.name); // "qqq946"
console.log(instance.age); // 17
  1. 借用构造函数问题
  • 如果仅仅是借用构造函数,也无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用无从谈起。

  • 而且在超类型的原型中定义的方法,对于子类型也是不可见的。

  • 综上,借用构造函数的技术也很少单独使用。

3. 组合继承

  • 组合继承,有时候也叫做伪经典继承。指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。
    • 思路:使用原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承。
    • JavaScript 中最常用继承模式。instanceof 和 isPrototype( ) 也能识别基于组合继承创建的对象。
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;
}
SubType.prototype = new SuperType(); // 继承方法
SubType.prototype.sayAge = function(){
  console.log(this.age);
}

var instance1 = new SubType('qqq946',17);
instance1.color.push('black');
cosnole.log(instance1.color);    // "red","blue","green","black"
instance1.sayName();             // 'qqq946'
instance1.sayAge();              // 17

var instance2 = new SubType('lqz',16);
console.log(instance2.colors);   // "red","blue","green"
instance2.sayName();             // 'lqz'
instance2.sayAge();             // 16

4. 原型式继承

  • 道格拉斯 · 克罗克福德的想法:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。他给出如下函数:
function object(o){
	function F(){ }
  // 临时性构造函数
  F.prototype = o;
  return new F();
}
  • 上面函数从本质上讲,object( ) 对传入其中的对象执行了一次浅复制。
var person = {
  name: 'qqq946',
  friends: ['qqq945','qqq947']
};

var anotherPerson = object(person);
anotherPerson.name = 'lqz';
anotherPerson.friends.push('zql');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'abc';
yetAnotherPerson.friends.push('cba');

console.log(person.friends); // 'qqq945','qqq947','zql','cba'
  • 这种原型式继承,要求必须有一个对象可以作为另一个对象的基础。
  • ES5 通过新增 Object.create( ) 方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。
    • 在传入一个参数的情况下, Object.create( ) 方法与 object( ) 方法的行为相同。
    • Object.create( ) 方法的第二个参数与 Object.defineProperties( ) 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
var person = {
  name: 'qqq946',
  friends: ['qqq945','qqq947']
}
/* 
var anotherPerson = Object.create(person);
anotherPerson.name = 'lqz';
anotherPerson.friends.push('zql');

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'abc';
yetAnotherPerson.friends.push('cba');

console.log(person.friends); // 'qqq945','qqq947','zql','cba'
*/
var anotherPerson = Object.create(person,{
  name: {
    value: 'lqz';
  }
})
console.log(anotherPerson.name); // 'lqz'
  • 在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

5. 寄生式继承

  • 寄生式继承是与原型式继承紧密相关的一种思路,同样由克罗克福德推而广之。

  • 思路与寄生构造函数和工厂模式类似:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function createAnother(original){
  var clone = object(original); // 通过调用函数创建一个新对象
  clone.sayHi = function(){    // 以某种方式增强这个对象
    console.log('hi');
  };
  return clone;               // 返回这个对象
}

var person = {
  name: 'qqq946',
  friends: ['qqq945','qqq947']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'
  • 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

  • 前面示范继承模式时使用的 object( ) 函数不是必须的。

  • 任何能够返回新对象的函数都适用于此模式。

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

6. 寄生组合式继承

  • 前面说过,组合继承是 JavaScript 最常用的继承模式,不过它的问题在于,无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
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;
}
SubType.prototype = new SuperType(); // 第一次调用
  • 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

    • 思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
    • 寄生组合继承的基本模式如下
    function inheritPrototype(subType,superType){
      var prototype = object(superType.prototype); // 创建对象
      prototype.constructor = subType();           // 增强对象
      subType.prototype = prototype;               // 指定对象
    }
    // 该函数实现了寄生组合式继承的最简单形式。
    
    function SuperType(name){
      this.name = name;
      this.color = ['red','blue','green'];
    }
    SuperType.prototype.sayName = function(){
      console.log(this.name);
    }
    function SubType(name,age){
      Super.call(this,name);
      this.age = age;
    }
    inheritPrototype(SubType,SuperType);
    SubType.prototype.sayAge = function(){
      console.log(this.age);
    }
    
    • 这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的多余属性。与此同时,原型链还能保持不变,因此能正常使用 instanceof 和 isPrototypeOf( )。
    • 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

小结

  • JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法。

  • 原型链的问题是对象实例共享所有继承的属性和方法,因此不宜单独使用,解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。

  • 使用最多的继承模式就是组合继承:使用原型链继承共享的属性和方法,通过借用构造函数继承实例属性。

  • 原型式继承:可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。

  • 寄生式继承:与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。

  • 寄生组合式继承:实现基于类型继承的最有效方式。

参考资料

  • 《JavaScript 高级程序设计》(第三版)