继承,原型链,借用构造函数,组合继承

    继承    

    继承是面向对象语言中的一个最为人津津乐道的概念,许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承方法签名,而实现继承则继承实际的方法。由于ECMAScript中函数没有签名(函数的参数类型和个数),没有重载(利用函数签名的不同来区别调用者到底调用的是那个方法!),所以无法实现接口继承,只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

    那接下来我们来学习一下原型链吧〜Come on〜

    原型链

    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;

};

    var instance = new SubType();

    alert(instance.getSuperValue()); // true

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

这个例子中实例、原型和构造函数之间的关系:

 

    在上面的代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型;这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法而且其内部还有一个指针,指向了SuperType的原型。最终结果就是这样的:instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法仍然还在SuperType.prototype中,但property则位于SubType.prototype中。这是因为property是一个实例属性,而getSuperValue()则是一个原型方法。既然SubType.prototype现在是SuperType的实例,那么property当然就位于该实例中了。此外,要注意instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写了的缘故。(这句话得好好理解~)

    通过实现原型链,本质上扩展了原型搜索机制,当以读者模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索SubType.prototype;3)搜索SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

 

    1、别忘记默认的原型

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

完整的原型链如下:

 

 

    2、确定原型和实例的关系

    可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceod操作符,只要用这个 操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。

    举个例子:

alert(instance  instanceof  Object);               //true

alert  (instance  instanceof  SuperType);        //true

alert  (instance  instanceof  SubType);           //true

    由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了true。

    第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isprototypeOf()方法也会返回true,如下所示:

    alert (Object.prototype.isPrototypeOf (instance));                  //true

    alert (SuperType.prototype.isPrototypeOf (instance));          //true   

    alert(SubType.prototype.isPrototypeOf (instance));               //true

 

    3、谨慎地定义方法

    子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后,即放在红体代码之后。

    举个例子:

    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;

};

    //重写超类型中的方法

    SubType.prototype.getSuperValue=function(){

    return false;

}

    var instance = new SubType();

    alert(instance.getSuperValue()); // false

    当通过SubType的实例调用getSuperValue()时,调用的就是这个重新定义的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。

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

    举个例子:

    

    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;

       } ,

     someOtherMethod:function(){

     return false;

   }
};

    var instance = new SubType();

    alert(instance.getSuperValue()); //error

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

 

    4、原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

    举个例子:

    function  SuperType(){

        this.colors=["red","blue","green"];

}

    function  SubType(){

}

    //继承了SuperType

    SubType.prototype=new SuperType();

    var instance1=new SubType();

    instance1.colors.push("black");

    alert(instance1.colors);             //"red,blue,green,black"

    var instance2=new SubType();

    alert(instance2.colors);               //"red,blue,green,black"

    这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有包含自己数组的colors属性。当SubTYpe通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的colors属性——就跟专门创建了一个SubType.prototype.colors属性一样。但结果是什么呢?结果是SubType的所有实例都会共享这一个colors属性。而我们对instance1.colors的修改能够通过instance2.colors反映出来,就已经充分证实了这一点。

    原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下, 给超类型的构造函数传递参数。因此,我们在解决问题是很少单独使用原型链。

        

    借用构造函数

    在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术,有时候也叫做构造对象或经典继承。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。

    function  SuperType(){

    this,colors=["red","blue","green"];

}

    function  SubType(){

    //继承了SuperType

    SuperType.call(this);

}

    var instance1=new SubType();

    instance1.colors.push("black");

    alert(instance1.colors);               //"red,blue,green,black"

    var instance2=new SubType();

    alert(instance2.colors);              //"red,blue,green"

    代码中加粗的那一行代码“借调”了超类型的构造函数。通过使用call()方法(或apply()方法也可以),我们实际上是在将要新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。

    1、传递参数

    相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

    举个例子:

    function  SuperType(name){

    this.name=name;                         //SuperType的实例属性name

}

    function  SubType(){

    //继承了SuperType,同时还传递了参数

    SuperType.call(this,"yao");

    //实例属性

    this.age=29;

}

    var  instance=new SubType();

    alert(instance.name);                      //"yao"

    alert(instance.age);                         //29

    以上代码中的SuperType只接受一个参数name,该参数会直接赋给一个属性。在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

    2、借用构造函数的问题

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

     

     组合继承

     组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承(继承共有的属性与方法),而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,有能够保证每个实例都有它自己的属性

    举个例子:

    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.constructor=SubType;

    SubType.prototype.sayAge=function(){

    alert(this.age);

};

    var instance1=new SubType("yao",29);

    instance1.colors.push("black");

    alert(instance1.colors);                                         //"red,blue,green,black"

    instance1.sayName();                                          //"yao"

    instance1.sayAge();                                             //29

    var instance2=new SubType("xiyao",19);

    alert(instance2.colors);                                         //"red,blue,green"

    instance2.sayName();                                          //"xiyao"

    instance2.sayAge();                                             //19

    在这个例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己属性——包括colors属性,又可以使用相同的方法了。

    组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。