代码改变世界

说不尽的函数-继承

2010-10-02 00:00  Feather  阅读(229)  评论(0)    收藏  举报

 最近花了不少时间在研究JavaScript的继承实现上面,看了很多文章,也翻了不少书。第一次接触是在看《JavaScript设计模式》的时候,里面用了一章详细讲解了三种继承的方法,虽然用了很多篇幅,但是我现在还是觉得它讲解得太跳跃了,我还是花了不少时间才勉强搞清楚。这两天在看《JavaScript高级程序设计》,又看到有一章讲述继承的,仔细一读,全然顿悟,这本书讲述得很详细,一步一步地深入。推荐一读。

把全部方法总结起来真的不简单,我用自己的理解把全部尽量整理一下吧。为了更清晰地讲解,我用一个案例作为线索:

 

一.案例

像很多Demo一样,我这里用Person作为父类,Student作为子类。Person有以下实例属性和原型属性:

function Person(nickname,phone)
{
//通过构造函数传参定义实例属性nickname
this.nickname=nickname||"no-name";
//通过特权实例方法访问phone
this.getPhone=function(){return phone||"no phone"}
}
//所有生成的实例都具有值为China的原型属性country
Person.prototype.country="China";
//所有实例都具有introduce原型方法
Person.prototype.introduce=function(){
return "hi,my name is "+this.nickname+",i come from "+this.country+" and my phone is "+this.getPhone();
}

 然后,我们的需求是:构造一个继承Person的Student类,增加一个major的参数作为实例属性,并且在introduce里面加上major的输出。

 

二.解决方案

下面我们将会针对以上问题,用几种解决分别尝试,分析各种方案之优劣,最后将会得出最佳方案。

1.使用call

在《javascript高级程序设计》一书中,这种方案称为“借用构造函数”技术(constructor stealing)。通过使用call或者apply方法在子类用this作为调用者调用父类构造函数,使子类的实例对象执行所有父类的初始化代码,从而实现继承父类的实例属性(当然,包括方法)。

对于案例中的情况,我们可以用下面代码:

function Student(nickname,phone,major)
{
Person.call(this,nickname,phone);
this.major=major;
}

 使用call的最大好处是,可以在子类构造函数中向父类构造函数传递参数,并且继承父类的实例属性。但是,缺点也是显而易见的,他无法无法实现对原型属性的继承,就拿案例来说,country属性和introduce方法无法在Student类中得以继承,如果都把他们变成实例方法,问题可以解决,但是那就破坏了案例中的前提条件了,而且只使用实例属性,就无法实现代码复用。事实上,案例构造的Person使用了各种属性,目的就是为了让我们找到一种均可实现实例属性和原型属性继承的完美解决方案。

 

2.使用原型链

关于原型链的概念大家应该都很清楚,在说不尽的函数-原型链一文中我也对此稍作探讨,欢迎赐教。说回使用原型链这种方案,具体实现其实也很简单:把子类中的prototype属性指向父类的一个实例。或者可以用下面一行代码表示:

Student.prototype = new Person();

 因为Student的prototype属性指向Person的一个实例,当实例化一个Student对象并调用其属性时,Javascript会先在查找Student对象是否存在该实例属性,若没有,则会继续在它的原型对象,即Student.prototype对象中查找,若还是没有,则Javascript会继续沿着原型链到Student.prototype的原型对象,即Person.prototype中找,如此类推。

有的人可能会问,为什么是把Person的一个实例赋给Student.prototype,而不直接用Person.prototype赋给它?这其实是继承概念上的问题,没错,如果这么做,Student类的确可以继承Person的原型属性,但是,如果我们要为子类添加一个原型属性时,这就也会破坏了父类的原型对象。

使用这种方案的好处是,子类可以同时继承父类的实例和原型方法;

而缺点有以下两点:

  1. 子类继承的实例属性是位于子类的原型对象中,也就是说Person中的实例属性变成了Student的原型属性,破坏了实例属性的意义。
  2. 无法使用构造函数传参实例化子类。

或者用下面代码可以说明得更清楚:

function Student(){}
				
Student.prototype = new Person();
var feather = new Student();
alert(feather.nickname);//继承了Person实例方法,显示"no-name";
				
var shadow = new Student();
alert(shadow.nickname)//显示"no-name"
				
Student.prototype.nickname="whose name";
				
alert(feather.nickname)//由于nickname是在原型对象中,这两个都显示"whose name"
alert(shadow.nickname)
				
feather.nickname="feather";//由于读写不对称,这样可以定义feather的实例属性,不影响shaodw
				
alert(feather.getPhone())//输出"no-phone",由于无法通过构造函数传参,我们无法重新定义phone

 

3.组合使用call和原型链

 上面两种方案都不能完美的实现继承,但是它们可以分别很好地实现继承实例方法和原型方法。所以,我们这Part讨论结合两者之长来实现继承,请看下面代码:

function Student(nickname,phone,major)
{
Person.call(this,nickname,phone);
this.major=major;
}
Student.prototype = new Person();

 首先,我们通过把Student的Prototype属性指向Person的一个实例来实现继承原型属性;再通过在初始化Student对象时,在构造函数里面首先调用Person构造函数来传参和继承实例属性。

我们要解决案例中的问题,还需要加上这句原型属性的定义:

Student.prototype.introduce=function(){
return "hi,my name is "+this.nickname+",i come from "+this.country+" and my phone is "+this.getPhone()+" and my major is "+this.major;}

 然后,我们用下面代码测试一下:

var feather = new Student("feather",1234567,"network")
alert(feather.introduce())
//输出hi,my name is feather,i come from China and my phone is 1234567 and my major is network

 所有一切似乎都完美无缺,测试也证明了这个方案可以解决案例中的需求。但是,里面其实还有不少可以优化的空间的,下面我们继续探讨。

在上一个解决方案中我们就知道,我们在实现原型继承这部分是通过把子类的prototype属性指向父类的一个实例。所以,在子类的prototype对象中存在父类的实例对象,上一个解决方案中也解释了为什么这些实例对象是没用的,所以我们这次用call方案来弥补这个缺陷,虽然Student对象和其原型对象都拥有Person的实例属性,但是实例对象中的实例属性会覆盖原型对象中的属性,所以我们不必担心上一个解决方案中的副作用。但是我们从代码维护角度看,这里出现了无谓的代码冗余——Student.prototype中由new Person()产生的实例属性(事实上,在这个新实例对象中我们只需要它的__proto__属性,用来构架原型链)。

另外一个问题是重定义prototype属性后constructor指向问题。下面代码将一并予以封装和优化:

function Inheritance(subClass,superClass)
{
function F(){};
F.prototype=superClass.prototype;
subClass.prototype=new F();
subClass.prototype.constructor=subClass;
}

写到这里估计大家都很清楚了,调用的代码我也不写了。这次的方案算是完美完成任务了,在《JavaScript设计模式》中,这种方案称为类式继承,《JavaScript高级程序设计》中则称为寄生组合式继承,两者代码稍有不同,但实质一样。下面,我们继续寻找其他的继承方案,待续...