js面向对象设计---继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,实现继承主要是依靠原型链来实现

一、原型链

原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

实现原型链有一种基本模式,其代码大致如下。

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

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

//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};
//true
var instance = new SubType(); 
alert(instance.getSuperValue());

以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,

默认的原型

前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。

确定原型和实例的关系

两种方式来确定原型和实例之间的关系。

  • 第一种方式: 是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。以下几行代码就说明了这一点。
//true
alert(instance instanceof Object);
//true
alert(instance instanceof SuperType);
//true
alert(instance instanceof SubType);
  • 第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true,如下所示。
//true
alert(Object.prototype.isPrototypeOf(instance));
//true
alert(SuperType.prototype.isPrototypeOf(instance));
//true
alert(SubType.prototype.isPrototypeOf(instance));
谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。

function SuperType(){
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function SubType(){
  this.subproperty = false;
}
//继承了
SuperTypeSubType.prototype = new SuperType();
//添加新方法 
SubType.prototype.getSubValue = function (){ 
  return this.subproperty; 
}; 
//重写超类型中的方法 
SubType.prototype.getSuperValue = function (){
  return false; 
}; 
const instance = new SubType();
//false
alert(instance.getSuperValue());

注意:即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链

function SuperType(){ 
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function SubType(){ 
  this.subproperty = false;
}
//继承了
SuperTypeSubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一行代码无效 
SubType.prototype = { 
  getSubValue : function (){return this.subproperty; },
  someOtherMethod : function (){ return false;} 
}; 
var instance = new SubType();
//error!
alert(instance.getSuperValue());

以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断——SubType和SuperType之间已经没有关系了。

原型链的问题
function SuperType(){
  this.colors = ["red", "blue", "green"];
}
function SubType(){}
//继承了
SuperTypeSubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
//"red,blue,green,black"
alert(instance1.colors);
var instance2 = new SubType();
//"red,blue,green,black"
alert(instance2.colors);

二、借用构造函数(伪造对象或经典继承)

基本思想:,即在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示:

function SuperType(name){
  this.name = name; 
  this.colors = ["red", "blue", "green"];}
  function SubType(){    
  //继承了SuperType    
  SuperType.call(this, 'xiaoming'); 
}
const instance1 = new SubType();
instance1.colors.push("black");
//"red,blue,green,black"
alert(instance1.colors);//"red,blue,green"
const instance2 = new SubType(); 
alert(instance2.colors);

借用构造函数的问题: 无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

三、组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function SuperType(name){
  this.name = name;    
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){ 
  alert(this.name);
};
function SubType(name, age){   
  //继承属性    
  SuperType.call(this, name);
  this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
  alert(this.age);
};
const instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
//"red,blue,green,black"
alert(instance1.colors);
//"Nicholas";
instance1.sayName();
//29
instance1.sayAge();
const instance2 = new SubType("Greg", 27);
//"red,blue,green"
alert(instance2.colors);
//"Greg"; 
instance2.sayAge();
instance2.sayName();

JavaScript中最常用的继承模式。而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

四、原型式继承

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

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。

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

const anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
const yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");
//"Shelby,Court,Van,Rob,Barbie"
alert(person.friends);

ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

const person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
const anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
const yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
//"Shelby,Court,Van,Rob,Barbie"
alert(person.friends);

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

五、寄生式继承

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

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

使用:

var person = {    
  name: "Nicholas",    
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
//"hi"
anotherPerson.sayHi();

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

六、寄生组合式继承
组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。


function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(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(){
  alert(this.age);
};

寄生组合式继承的基本模式如下所示。

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

七 Object.create() (es5提供的方法)

方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
语法

Object.create(proto, [propertiesObject])
  • proto: 新创建对象的原型对象。
  • propertiesObject: 可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。

八、es6语法糖实现继承

移步es6教程

参考文献。javascript 高级编程第3版

    原文作者:EdmundChen
    原文地址: https://www.jianshu.com/p/f9084afeb18f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞