6.3继承
6.3.1 原型链
1、原型链的概念、示例、原理
- 原型链是作为实现继承的主要方法。
- (回顾一下)构造函数、原型和实例的关系:每 个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型 对象的内部指针。
- 所谓原型链的基本概念:让原型对象等于另一个类型的实例。
- 实现原型链有一种基本模式,其代码大致如下。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType ,重写了SubType.prototype,此时SubType.prototype的constructor属性已经不指向构造函数Subtype了,二而是作为SuperType的实例指向SuperType.prototype
SubType.prototype = new SuperType();
//在继承了 SuperType 的属性和方 法的基础上又添加了一个新方法
SubType.prototype.getSubValue = function (){
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());//true
- 实现的本质是
重写原型对象
,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。以下是上面例子中的实例以及构造函数和原型之间的关系如图:
- 要注意 **instance.constructor 现在指向的 是 SuperType,**这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故1。 【实际上,不是 SubType 的原型的 constructor 属性被重写了,而是 SubType 的原型指向了另一个对象—— SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。】(实例的constructor == 实例的原型的constructor ,如:instance.constructor == SubType.prototype.constructor)
- 在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用 instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype; 3)搜索 SuperType.prototype,最后一步才会找到该方法。
2、别忘记默认的原型
- 所有引用类型默认都继承了 Object,而 这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例(在最顶端找到Object原型),因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、 valueOf()等默认方法的根本原因。【一句话,**SubType 继承了 SuperType,而 SuperType 继承了 Object。**当调用 instance.toString()时,实际上调用的是保存在 Object.prototype 中的那个方法。】
3. 确定原型和实例的关系
有两种方式可以确定原型和实例之间的关系:第一种方式是使用 instanceof 操作符
,第二种方式是使用 isPrototypeOf()方法
。
- instanceof 操作符:只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。(只要能顺着原型链找到这个构造函数,就返回true)
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
- isPrototypeOf()方法:同样,只要是原型链中出现过的原型,都可以说是该 原型链所派生的实例的原型
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
4. 谨慎地定义方法
- 给原型添加方法的代码一定要放在替换原型的语句之后。(如果在之前就会被替换掉)
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;
};
//重写超类型中的方法 【并不是修改了超类型(SuperType.prototype)中的方法,而是在SubType.prototype中添加了一个方法】
SubType.prototype.getSuperValue = function (){
return false;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //false
//通过SuperType 的实例调用 getSuperValue()时,还会继续调用原来的那个方法。即原来位置上的方法并没有被改写或替换,只是被屏蔽了。
var instance1 = new SuperType();
console.log(instance1.getSuperValue());
注意:给一个对象添加一个自身并不具有的属性,而原型却具有同名的属性,那么结果不是改变原型对象上的属性,而是给这个对象自身添加了这个同名属性!原型上的该属性,并不会改变!只是在访问实例的时候被屏蔽!
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这 样做就会重写原型链,如下面的例子所示。
5、原型链的问题
- 最主要的问题:原型中包含引用类型值。包含引用类型值的原型属性会被所有实例共享;在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
如下所示:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black"); //修改的是SubType原型上的colors属性
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
- 原型链的第二个问题是:在创建子类型的实例时,**不能向超类型的构造函数中传递参数。**实际上, 应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上 前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。(有疑问?)
6.3.2 借用构造函数
- 使用的目的:解决原型中包含引用类型值所带来的问题。(所有实例共享原型属性)
- 基本思想:在子类型构造函数的内部调用超类型构造函数,通过使用
apply()
和call()
方法。 - 与原型链方式比较:原型链方式操作的是原型,借用构造函数方式操作的是构造函数。如下所示:
在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。结果, SubType 的每个实例就都会具有自己的 colors 属性的副本了。
1. 传递参数
- 借用构造函数优点:相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
- 在
SubType 构造 函数
内部调用 SuperType 构造函数
时,实际上是为 SubType 的实例设置了 name 属性。 - 为了确保 SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中 定义的属性。
2. 借助构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定 义,因此函数复用就无从谈起了。
6.3.3 组合继承
- 组合继承(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); //包括继承name和color属性
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
//每创建一个SubType实例,就调用一次SuperType构造函数,修改的是创建的当前实例下的colors属性。
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1);
instance1.sayName();//"Nicholas";
instance1.sayAge();//29
var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);//"red,blue,green"
instance2.sayName();//"Greg";
instance2.sayAge();//27
- 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继 承模式。而且,
instanceof 和 isPrototypeOf()
也能够用于识别基于组合继承创建的对象。
6.3.4 原型式继承
- 思路/目的:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。(在这个过程中没有构造函数什么事,没有新建自定义类型,是原型和示例之间的关系)。
- 为了达到上面的目的,道格拉斯·克罗克福德给出了如下函数:在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。(返回传入对象的实例,传入对象作为原型)
function object(o){
function F(){}
F.prototype = o;
return new F();
}
- 示例:接上代码
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";//在实例中 添加属性name
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
-
Object.create()
方法:ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。【用Object.create()就不用提前定义函数object()了】- 这个方法接收两个参数:一个是用作新对象原型的对象和(可选的)一个是为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。
- 如下:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); //包含引用类型值的属性始终都会共享相应的值就像使用原型模式一样。 alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()
方法的第二个参数与Object.defineProperties()方法
的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); alert(anotherPerson.name); //"Greg"
6.3.5 寄生式继承
- 寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数。(也是传入作为原型的对象,返回新对象实例)
- 形式上:寄生式继承就是把原型式继承再次封装,然后在对象上扩展新的方法,再把新对象返回
可以像下面这样来使用 createAnother()函数:
- 前面示范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
6.3.6 寄生组合式继承
1. 组合继承模式的缺点
前面说过,组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。
- 组合继承的示例:在第一次调用 SuperType 构造函数时, SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这 一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
- 图示:
- 调用两次 SuperType 构造函数的结果:有两组 name 和 colors 属性,一组在实例上,一组在 SubType 原型中。
2. 寄生组合式继承
-
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型 的原型。寄生组合式继承的基本模式如下所示。
-
这个示例中的
inheritPrototype()函数
实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。 最后一步,将新创建的对象(即副本)赋值给子类型的原型。 -
理解难点:指的是第三步,将prototype赋值给subType的原型,由于创建的副本(superType.prototype的实例)中没有constructor属性,为了使subType.prototype中的constructor属性不变,所以要提前在第二部添加constructor属性。用这样的方式将父类型的实例赋值给子类型的原型,但却不用再调用一次父类型构造函数
-
这样,我们就可以用调用 inherit- Prototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了,例如:
-