JavaScript高级 面向对象的程序设计 (二)《JavaScript高级程序设计(第三版)》
二、继承
OO是面向对象语言最为有魅力的概念。一般的OO语言都实现了两种继承,接口继承和实现继承。接口继承只继承方法签名,而实际继承继承了实际的方法。
而在JS中,函数没有签名,所以无法实现接口继承。只能依靠原型链--实现继承。
2.1原型链
JS中描述了原型链的概念,并利用原型链作为实现继承的主要方法。
其基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法。
functionSuperF(){
this.superPropty ='B';
}
SuperF.prototype.getSuperPropty =function(){
alert(this.superPropty);
}
functionSubF(){
this.subPropty ='S';
}
SubF.prototype =newSuperF();
SubF.prototype.constructor =SubF
var p =newSubF();
p.getSuperPropty();
重写子类的原型对象,代之以父类的实例。那么这个实例拥有一个指向父类构造函数原型的指针。
2.1.1别忘记默认的原型
完整的原型链
2.1.2确定原型和实例的关系
确定引用类型是否在原型链中出现过
2.1.3谨慎的定义方法
在子类中定义新方法属性或者重写父类中的方法属性,必须在子类原型被替换之后。
ps:需要注意的一定是再通过原型链实现继承时,不能使用对象字面量添加或重写方法或属性。
这样会使原型再次替换成字面量而导致前面的操作无效。
2.1.4原型链的问题
原型链虽然强大,但是它也带来一些问题:其中最主要的问题来自包含引用类型值的原型。因为包含的引用类型值的原型,其属性会被所有实例共享。所以我们通常把属性写在构造函数中,来避免这个问题。
于是,原型实际上会变成另一个类型的实例,那么原型实例的属性就成了原型属性了。这就会带来问题。
第二个问题是:在创建子类实例的时候,不能向超类型的构造函数中传递参数。这样会影响所有对象实例。
所以,结合上面两个问题,我们通常很少单独使用原型链。
2.2借用构造函数
这是一种解决原型中包含引用类型值所带来问题的技术。(又叫做伪造对象)
这种技术的基本思想:在子类型构造函数的内部调用超类构造函数。(使用apply()/call()方法)
functionSuperF(){
this.superPropty ='B';
this.colors =['red','blue'];
}
functionSubF(){
SuperF.call(this);
this.subPropty ='S';
}
var p =newSubF();
p.colors.push('black');
p.superPropty ='C'
alert(p.superPropty);//C
alert(p.colors);//'red','blue','black'
var pp =newSubF();
alert(pp.superPropty);//B
alert(pp.colors);//'red','blue'
2.2.1传递参数
functionSuperF(name){
this.name = name;
this.colors =['red','blue'];
}
functionSubF(){
SuperF.call(this,'zjh');
this.subPropty ='S';
}
var p =newSubF();
p.colors.push('black');
alert(p.name);//zjh
alert(p.colors);//'red','blue','black'
var pp =newSubF();
alert(pp.name);//zjh
alert(pp.colors);//'red','blue'
这是借用构造函数模式的一个很大的优势。
ps:为了防止超类构造函数不会重写类型的属性,可以在调用超类型构造函数之后,再添加子类型构造函数的属性。
2.2.2借用构造函数模式的问题
如果仅仅是借用构造函数,那么也无法避免构造函数模式存在的问题----方法都在构造函数中定义,因此函数复用就不可能了。在超类构造函数的原型中定义方法,对子类而言是不可见的,结果所有的类型都只能使用构造函数模式。
所以这种技术也是很少单独使用的。
2.3组合继承
组合继承有时也叫作经典继承。指的是:利用原型模式和借用构造函数模式相结合,发挥两者之长的继承模式。
/**
* 组合继承
*/
functionSuperF(name){
this.name = name;
this.colors =['red','blue'];
}
SuperF.prototype.getName =function(){
alert(this.name+'+function');
}
functionSubF(){
SuperF.call(this,'zjh');
this.subPropty ='S';
}
SubF.prototype =newSuperF();
var p =newSubF();
p.colors.push('black');
alert(p.name);//zjh
alert(p.colors);//'red','blue','black'
p.getName();//zjh+function
var pp =newSubF();
pp.name='zzz'
alert(pp.name);//zjh
alert(pp.colors);//'red','blue'
pp.getName();//zzz+function
组合继承避免了原型链和借用构造函数的缺陷,成为一种JS最常用的继承模式。同时支持instanceof和isPrototypeOf()
2.4原型式继承
这种实现继承的方法没有使用严格意义上的构造函数,而是借用原型,基于已有对象创建新的对象,同时还不必因此创建自定义类型。
function getObject(o){
function F(){};
F.prototype = o;
return new F();
}
var o = {
name : 'zzz',
age : '15',
colors: ['red','blue']
}
var o1 = getObject(o);
o1.colors.push('black');
alert(o1.colors);
var o2 = getObject(o);
alert(o2.colors);从何本质上来讲,getObject()对传入其中的对象执行了一次浅复制。
ps:JS也提供了一个函数create(),当它只有一个参数的时候,实现了和上面getObject()方法相同的效果。
var o3 = Object.create(o);alert(o3.name)
第二个参数:一个为新对象定义的额外属性的对象
var o4 = Object.create(o,{
name : {value:'zsda'}
});
alert(o4.name) 但是无论用getObject()还是create() ,原型继承所带来的负面影响一定会有(引用类型值在所有创建的对象中共享),但是如果只想让一个对象与另一个对象保持类似的情况下,这种原型集成模式是完全可以胜任的。
2.5寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似。即 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function getObject(o){
function F(){};
F.prototype = o;
return new F();
}
var o = {
name : 'zzz',
age : '15',
colors: ['red','blue']
}
function getAnother(o){
var clone = getObject(o); //通过调用一个函数 返回复制的对象
clone.getName = function(){ //对对象增强
alert(this.name);
}
return clone;
}
var o1 = getAnother(o);
o1.getName(); 这种方式主要利用于不考虑自定义类型和构造函数的情况下。
它的缺陷:和构造函数模式一样,每创建一次对象,那么它的方法都无法复用,必须创建一个新的方法,效率相对差一些。
2.6寄生组合式继承
上面说过 组合继承是JS中最常见的实用的继承模式,不过它也有自身的不足。
组合继承最大的问题就是:在任何情况下都会调用两次超类型构造函数,一次是在创建子类的原型时候,第二次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
function SuperF(){
this.name = 'zjh';
this.age = 15
}
SuperF.prototype.getName = function(){
alert(this.name);
}
function SubF(){
SuperF.call(this); //第二次调用
this.colors = ['red','blue'];
}
SubF.prototype = new SuperF(); //第一次调用
SubF.prototype.constructor = SubF;
var o = new SubF();
o.getName(); 从上面的代码可以看出name和age属性被两次得到,一次在实例上,一次在SubF原型上。这就是组合继承的弊端
解决它的方法就是----寄生组合继承
所谓寄生组合继承,就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。主要思路是:
不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subF,superF){
var prototype = object(superF.prototype); //创建超类对象原型的副本
prototype.constructor = subF;//增强对象
subF.prototype = prototype;//指定对象
}
function SuperF(){
this.name = 'zjh';
this.age = 16;
this.colors = ['red','blue'];
}
SuperF.prototype.getName = function(){
alert(this.name);
}
function SubF(){
SuperF.call(this);
this.sex = '男';
}
inheritPrototype(SubF,SuperF);
var o = new SubF();
o.colors.push('black');
o.getName();
alert(o.colors);
var o1 = new SubF();
alert(o1.colors); 这种方式的高效体现在只调用一次SupeF构造函数,并且因此避免了在SubType.prototype上面创建不必要的多余的属性,与此同时,原型链还能保持不变,因此能够正常使用instanceof 和 isPrototypeOf()。这种寄生组合式继承是引用类型最理想的继承范式。
ps:YUI中的YAHOO.lang.extend()方法就采用了这种方式。
三、小结
JS支持面向对象(OO)的编程,但不使用类或者接口来实现。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。
3.1几种创建对象的方法:
工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回一个对象。这个模式因为无法被识别类型,只能被识别为object,而被构造函数模式所取代。
构造函数模式:可以创建自定义的引用类型,可以像创建内置对象
实例一样使用new操作符。不过,它的缺点就是它的每个成员都没办法复用,特别是函数,一方面效率低,一方面函数可以不局限于任何对象,因此没有理由不在多个对象间共享函数。
原型模式:使用构造函数的prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式。一般使用构造模式定义实例属性,使用原型模式定义共享的属性和方法。
3.2几种继承方式:
JS主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型来实现的。
原型链继承:导致引用类型值会被所有创建的对象共享。
借用构造函数继承(call/apply):导致方法都必须在构造函数内部定义,这样方法的复用就无从谈起了。
组合继承:利用上面两种继承的有点组合起来。缺陷是:会调用两次超类型构造器,从而重复定义属性。
原型式继承(create):它实际上也是原型链继承,问题也是引用类型值会被所有创建的对象共享。(可以不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制,得到复制后的副本可以进一步改造)
寄生式继承:在原型式继承的基础上,基于某个对象创建一个新对象,然后增强对象,返回对象。缺陷和借用构造函数继承一样,方法无法复用共享。
寄生组合式继承:结合寄生式继承和组合继承,解决了两种继承的缺陷,是最有效的方式。