对JS中继承的思考

名词理解:

  prototype属性,是函数对象特有的属性,不存在其他对象中(函数也是对象)。每当创建一个新函数,该函数就会自动包含一个prototype属性,这个属性用来指向函数的原型对象。

  prototype对象,即原型对象。 原型对象里包含着实例中需要共享的属性和方法。这里要注意,由于原型对象本身就是对象,那么默认的原型对象也是Object对象的实例。

  [prototype]指针,是ECMAScript 5中定义的一个名称,是构造函数创造的新实例中指向构造函数原型对象的指针,也就是部分浏览器中可见到的__proto__属性。

  见如下例子:

function Foo(){}    
//构造函数

var proto = Foo.prototype
//Foo具有prototype属性,指向原型对象proto

var bar = new Foo();
bar.__proto__ == Foo.prototype;
//对象的实例bar具有__proto__属性,指向Foo的原型对象

//因为proto和bar两个变量指向的不是函数,因此没有prototypt属性

 

继承的方式的理解:

  如代码所示,一个简单的继承例子。父对象Father有两个属性,基本类型name字符串和引用类型friends数组,同时在其原型对象中有个sayHello的共享方法。子对象Son通过语句 “ Son.prototype = new Father(); " 继承了Father对象,并在构造函数中调用了Father对象的构造函数,同时Son的原型对象中有个sayFather的共享方法。

 1 function Father(name){
 2     this.name = name || 'Jberry';
 3     this.friends = ['Lee','Alice','Tom'];
 4 }
 5 Father.prototype.sayHello = function(){
 6     alert(this.name + ' : Hello world!');
 7 };
 8 
 9 function Son(name){
10     Father.call(this,name);
11 }
12 Son.prototype = new Father();
13 Son.prototype.sayFather = function(){
14     alert(this.name + ' : Hei! Father!');
15 };

  看了原型式继承的都了解,“ Son.prototype = new Father(); "是继承步骤中最为关键的一环,那么是如何实现的呢?

  我们先来看new操作符,以“ var _tom = new Father();” 为例,实际上执行了三个操作:

var _tom = {}; 
//生成一个_tom空对象                                                     
_tom.__proto__ = Father.prototype;    
//将_tom对象中的__proto__指向Father的原型对象            
Father.call(_tom);
//在_tom对象的域中调用Father构造函数,进行实例属性的初始化

  很显然,new主要实现了两个步骤:原型对象的指定,以及构造函数的调用。

  那么“ Son.prototype = new Father(); "的作用就很明显了,类似于“ Son.prototype =_tom; ”。首先它将Son的原型对象中的__proto__指向了Father的原型对象,即“ Son.prototype.__proto__ = Father.prototype; ";其次在Son的原型对象中调用Father的构造函数,即“ Father.call(Son.prototype); "。这样,Son的原型对象里既包含了Father的原型对象(这里是包含了指向Father原型对象的指针__proto__),也包含了Father构造函数调用后的实例属性与实例方法。好消息是,Son实现了对Father属性方法的完全继承;坏消息是,Father的实例属性和方法也包含在Son的原型对象里,成了Son的公共方法。

  为了不继承父对象的实例属性和方法,同时考虑到原型对象本身就是一个对象,那能不能直接用“ Son.prototype = Father.prototype; "来单独继承Father的公共属性和方法?

Son.prototype = Father.prototype;
Son.prototype.constructor = Son;

  先来分析一下,这条语句的意思是将Father的原型对象赋给了Son的原型对象,那么Son的原型对象里包含的都是Father的公共属性和方法,这是符合我们的初衷的。同时,由于Son的原型对象的重写,此时Son.prototype.constructor 是指向Father的,我们添加一条语句 “ Son.prototype.constructor = Son; “,完成任务了。看似完成了,但我们忽略了一个重要的东西,那就是引用!上面提到原型对象本身是个对象,而构造函数的prototype属性实际上是一个指向原型对象的引用!如果在Son中修改原型对象,那么也会修改Father的原型对象!例如例子中,就会在Father的原型对象中添加sayFather()方法!子对象会修改父对象的内容,这是面向对象编程中绝对不允许的。那么如何进行改进呢?我们看到《Javascript设计模式》中给出的一个extend()方法:

function extend(subClass, superClass){
    function F(){}
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
}
extend(Son,Father);

  在extend函数中,通过F构造函数作为桥梁。如同上面的分析方法对语句进行拆分得到:" F.prototype = Father.prototype; ", " Son.prototype.__proto__ = F.prototype; ",由于F构造函数为空,没有实例的属性的方法,那么综合上面两个式子得到:Son.prototype.__proto__ === Father.prototype;。可见Son的原型对象通过__proto__指向Father的原型对象,两个原型对象得以分开,单独的操作将不影响对方的原型对象。同时F构造函数的介入,也只对Father中原型对象里的公共属性和方法做出了继承,移除了实例的属性和方法。

  在如果有var _bob = new Son();,即生成_bob的Son对象实例。因为实例对象_bob里有_bob.__proto_ === Son.prototype,那么通过上面的推断,有_bob.__proto__.__proto__ === Father.prototype;。同时,在上面名词解释中已经说到 “由于原型对象本身就是对象,那么默认的原型对象也是Object对象的实例 “,由于Father的原型对象未被改写、属于默认的,那么将其看成一个实例,有Father.prototype._proto_ === Object.prototype,这也解释了为什么自定义的对象能用原生Object对象的方法了。综合上述的式子:_bob.__proto__.__proto__.__proto__ === Object.prototype;,一条完整的,由__proto__指针构成的原型链就完成了!就像配图的一样~

  至此,相对完美的继承方法已经得到,但还有一点瑕疵。如果想继承父对象的实例的属性和方法,还必须调用父对象的构造函数,如例子中的“ Father.call(this,name); "这样的做法是在子对象的构造函数中固化了父对象的名词,增加了父对象与子对象的耦合,一旦改变要继承的父对象,将不便于调试。因此《Javascript设计模式》中对extend方法做出了扩充,并给子对象添加的superProto属性,指向父对象的原型对象,那么就可用superProto.constructor调用父对象的构造函数了!如下:

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

    subClass.superProto = superClass.prototype;
    if(superClass.prototype.constructor === Object.prototype.constructor){
        superClass.prototype.constructor = superClass;
    }
}

//在Son的构造函数里可改成
Son.superProto.constructor.call(this,name);

  extend()函数的最后3行是为了确保父对象的原型对象的constructor属性已经设置,即便父对象就是Object对象本身(这里有点疑惑,在firefox里看这个constructor属性已经指向Object()了)。

  最后再说一个例子:

var Father = {
    name : 'Jberry',
    friends : ['Lee','Alice','Tom'],
    sayHello : function(){
        alert(this.name + ' : Hello world!');
    }
 };

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

var son = extend(Father);
//son.name = ...;
//son.sayFather = function(){...};

  Father这时不是一个构造函数,而是一个单纯的字面量对象了。新的extend函数将Father直接作为原型对象赋给了F.prototype,然后son继承了Father。看到这里,我才感觉到“ 原型式 ”继承的含义。正如某博客里说到,原型式继承就像“ 依葫芦画瓢 “,原型是” 葫芦 ",瓢是在原型的基础上增强的对象。这里的Father中的东西都在son的原型中,就像父亲传授的东西就像一个原型牢牢“ 印 "在儿子的心中,儿子也会有自身的改变,这可能才是原型最终的意义吧……

 

constructor的属性:

  constructor是原型对象的一个属性,它用来指向自动生成原型对象的那个构造函数。

function F(){}
F.prototype.constructor = F;

  来讨论一下constructor的生存周期。由于在定义构造函数的时候,自动生成了函数的原型对象,constructor就是原型对象中的一个自动生成的属性,此时已经默认指向该构造函数了。一旦重写了原型对象,constructor就不再存在了,这时JS就会延着原型链向上搜索constructor属性了(我是这么理解的)。例子中:

Son.prototype = new Father();   //Son.prototype.constructor == Father

  此时Son的原型对象被重写,里面的constructor属性不存在了,此时JS会延着原型链(即__proto__链)向上搜寻,因为Son.prototype.__proto__ === Father.prototype,搜寻到Son.prototype.__proto__.constructor === Father,因此Son的原型对象的constructor指向Father。

  因此为了保证由Son生成的实例的类型,手工加上:

Son.prototype.constructor = Son;

  此时,会有两个constructor了(一个是Son.prototype.constructor,另一个是Son.prototype.__proto__.constructor),按照从原型链的底层搜索原则,此时的constructor已指向Son了。

  至此,只要注意每次在重写原型对象的时候,特别是字面量的写法以及实现继承的时候,手工给constructor赋值就ok了。

 

  打完手工!

  上面的解释都在firebug里验证了一下,有一些自己的理解,欢迎批评指正!

posted @ 2013-04-12 18:18  蓝莓调调  阅读(1092)  评论(2编辑  收藏  举报