javascript支持实现继承,也就是继承父类的方法和属性。(注:这边继续沿用前面说过的,类的概念。具体看【javascript原型对象、构造函数和实例对象】中的说明。)
原型链
原理
原型链实现继承的基本思路:利用原型让一个引用类型继承另一个引用类型的属性和方法
那么怎么做到这种继承呢?方法很简单,就是让子类的原型等于父类的一个实例。这样一来,子类的原型中就拥有一个指向父类原型的内部指针(这里涉及到原型对象、构造函数和实例之间的关系,具体的可以参看【javascript原型对象、构造函数和实例对象】)。自然就继承了它的方法和属性。来看一个例子:
//定义一个人的类
function Person(){
this.name="Tom";
this.age=10;
}
Person.prototype.sayName=function(){
return this.name;
}
//定义一个小孩的类,暂时不拥有属性和方法
function Child(){}
//设置Child类的原型为Person的实例
Child.prototype=new Person();
//设置Child类的方法
Child.prototype.sayAge=function(){
return this.age;
}
//创建Child的一个实例
var p1=new Child();
alert(p1.sayAge()); //10
alert(p1.sayName()); //Tom
alert(Child.prototype.constructor==Child); //false
alert(Child.prototype.constructor==Person); //true
alert(p1.constructor==Child); //false
alert(p1.constructor==Person); //true
上面这个例子,显示定义了一个Person类,它拥有两个属性(name、age)和一个方法(sayName())。然后定义了一个Child类,它暂时不拥有属性和方法。接着,将Child的prototype指向Person类的一个实例,这就实现了继承。在这之后,为Child的原型添加了方法。
从alert语句中可看出,Child的实例p1,继承了Person的属性和方法。
其实,在上面的例子中,还忽略了一个环,那就是Person继承了Object,这个继承也是通过原型链实现的。这样一来,上面那个例子的继承关系可用下图来表示:
其实上图所展示的也就是一条原型链。(这里省略了构造函数的指针关系。因为通过原型的指向也可以得到构造函数的指向,所以为了简洁,这里就不在指出。)
用文字,原型链也可以这样表示的:
/*
*原型链:
*p1 [Child的实例]
* Child.prototype [Person的实例]
* Person.prototype
* {sayName:...}
* Object.prototype
* {toString:...}
*/
注意:
- 给子类原型添加方法或属性一定要放在替换原型的语句之后。在上面的例子中,就是要在Child.prototype=new Person()语句之后。
- 用instanceof检测在原型链中出现过的构造函数与实例之间的关系,都将返回true。
- 用isPrototypeOf()方法检测原型链中出现过的原型与实例之间的关系,也都将返回true。
- 用原型链来实现继承的方法中,不能使用对象字面量来创建子类原型的方法或属性。因为这样会重写了原型,切断了它与父类的联系,也就不存在继承关系了。
还用上面的例子,我们来验证下,注意项中的2,3点:
//定义一个人的类
function Person(){
this.name="Tom";
this.age=10;
}
Person.prototype.sayName=function(){
return this.name;
}
//定义一个小孩的类,暂时不拥有属性和方法
function Child(){}
//设置Child类的原型为Person的实例
Child.prototype=new Person();
//设置Child类的方法
Child.prototype.sayAge=function(){
return this.age;
}
//创建Child的一个实例
var p1=new Child();
alert(p1 instanceof Object); //true
alert(p1 instanceof Person); //true
alert(p1 instanceof Child); //true
alert(Object.prototype.isPrototypeOf(p1)); //true
alert(Person.prototype.isPrototypeOf(p1)); //true
alert(Child.prototype.isPrototypeOf(p1)); //true
同样也看一个例子,来验证注意项中的第4点:
//定义一个人的类
function Person(){
this.name="Tom";
this.age=10;
}
Person.prototype.sayName=function(){
return this.name;
}
//定义一个小孩的类,暂时不拥有属性和方法
function Child(){}
//设置Child类的原型为Person的实例
Child.prototype=new Person();
//用对象字面量添加新方法,会导致上面一行代码无效
Child.prototype={
sayAge:fucntion(){
alert(this.age);
}
};
//创建Child的一个实例
var p1=new Child();
alert(p1.sayAge()); //error!
用对象字面量的形式向Child.prototype中添加方法,现在原型中包含的将是Object中的实例,而不是Person中的实例,将导致错误。
属性搜索机制
在javascript中,对象属性的搜索是先从实例开始的,然后逐级向上搜索,直到找到相应的属性或者直到原型链的末端(即原型为null)才停止。
还是那前面的例子来说明。执行alert(p1.sayName()),搜索将分为以下几个步骤:1)搜索p1;2)搜索Child.prototype;3)搜索Person.prototype,最后一步才找到该方法。假如要执行alert(p1.sayAge()),则只要经历两个步骤即可,因为sayAge()存在与Child.prototype中,在这里找到了,就不必再去搜索Person.prototype了。
由上面的搜索机制也可以看出,假如在子类中重写了父类的属性(或方法),将屏蔽父类中同名的属性(或方法)而以子类重新定义的属性(或方法)替代之。
缺点
用原型链来实现继承会存在一些问题。这个问题主要是由于:原型中的属性是被所有实例所共享的。假如包含了引用类型值的原型,那么就会导致一些我们不希望看到的现象:
//定义一个人的类
function Person(){
this.friends=["Lily"];
}
//定义一个小孩的类,暂时不拥有属性和方法
function Child(){}
//设置Child类的原型为Person的实例
Child.prototype=new Person();
//创建Child的实例
var p1=new Child();
var p2=new Child();
alert(p1.friends); //Lily
alert(p2.friends); //Lily
p1.friends.push("Sam");
alert(p1.friends); //Lily,Sam
alert(p2.friends); //Lily,Sam
在上面的例子中,我们只为p1添加一个朋友,却导致p2也同时增加了这个朋友,这不是我们想要的。那为什么会导致这样的结果呢?
原因就是,当Child通过原型链继承了Person,Child.prototype就变成了Person的一个实例,它就拥有了friends这个属性,这就相当于Child.prototype自己添加了friends属性一样。我们知道,添加在Child.prototype中的属性是被所有Child的实例所共享的。所以,当p1改变了friends这个属性的值,这个结果也将立即反应到p2中。
用原型链实现继承还有另一个问题,那就是子类不能向父类传递参数。
借用构造函数
原理
借用构造函数来实现继承的基本思路是:在子类的构造函数中调用父类的构造函数。主要通过call()方法或apply()方法来实现这种调用。
function Person(name){
this.name=name;
this.friends=["Lily"];
}
function Child(){
Person.call(this,"Tom"); //向父类传递参数
}
var p1=new Child();
alert(p1.friends); //Lily
alert(p1.name); //Tom
var p2=new Child();
alert(p2.friends); //Lily
p1.friends.push("Sam");
alert(p1.friends); //Lily,Sam
alert(p2.friends); //Lily
看上面的例子,在Child类中向Person类传递了名字参数,通过调用call()方法,实现在Child实例的环境中调用Person构造函数,这样就会在Child上实现Person()中所有对象的初始化,最终结果就是Child的每个实例都拥有一份Person中属性的副本,从而实现继承。
优点
上面的例子中,实现了在Child中向Person传递参数,并且,由这种方式实现的继承,是让Child每个实例都拥有一份friends属性的副本,这样当p1修改friends属性的值时,实际上修改的是Person中friends属性的副本,不会影响到其他实例。这么说来,借用构造函数实现继承就有一下两个有点:
- 可以实现在子类构造函数中向父类构造函数传递参数;
- 子类的每个实例中都拥有父类属性的一个副本,即实例属性相对独立,不会互相影响。
缺点
借用构造函数实现继承也存在问题,就是方法都必须在构造函数中定义,原因在于父类原型中定义的方法,在子类中是不可见的。这样一来,函数的复用就无从谈起了。
function Person(name){
this.name=name;
}
Person.prototype.sayName=function(){
return this.name;
}
function Child(){
Person.call(this,"Tom");
}
var p1=new Person("Lily");
alert(p1.sayName()); //Lily
var p2=new Child();
alert(p2.sayName()); //error!
上面这个例子,alert(p2.sayName());将导致错误。因为对于Child来说,它从未定义这样的一个方法,而且并不能从父类中继承。
基于上面原因,也很少单独用这种方式实现继承。
组合继承
原理
所谓组合继承,就是将原型链和借用构造函数的技术组合到一起,使用原型链实现对原型属性的继承,而通过借用构造函数来实现对实例属性的继承。
function Person(name){
this.name=name;
this.friends=["Lily"];
}
Person.prototype.sayName=function(){
return this.name;
}
function Child(name,age){
Person.call(this,name);
this.age=age;
}
Child.prototype=new Person();
Child.prototype.constructor=Child; //将指针重新指向Child
Child.prototype.sayAge=function(){
return this.age;
}
var p1=new Child("Tom",10);
alert(p1.sayName()); //Tom
alert(p1.sayAge()); //10
alert(p1.friends); //Lily
var p2=new Child("Sam",5);
alert(p2.sayName()); //Sam
alert(p2.sayAge()); //5
alert(p2.friends); //Lily
p1.friends.push("Bob");
alert(p1.friends); //Lily,Bob
alert(p2.friends); //Lily
缺点
看上面的例子,Person被调用了两次,第一次是在创建Child原型的时候,将Child.prototype作为Person的一个实例,这时Child.prototype会得到两个实例属性(name和friends),它们位于Child的原型中,是被所有Child实例所共享的;第二次是在创建Child实例时,调用了Child构造函数,它的内部就也调用了Person,这时Child的每个实例都拥有了Person中属性的副本,于是,这两个属性(name和friends)就覆盖了原型中的两个同名属性。等于说,每一次在调用Child构造函数时,都会产生两组name和friends属性:一组在Child原型中,一组在实例上。造成资源浪费。
寄生组合式继承
为了避免组合继承的确定,就有了寄生组合式继承的方法。
原理
寄生组合式继承的基本思路是:通过借用构造函数来继承属性,通过原型链混成形式来继承方法。本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型。
寄生式继承
在讲寄生组合式继承前,先来了解下寄生式继承。
所谓寄生式继承,就是创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回该对象。
首先,它会借用一个函数来实现类似原型链的继承,只不过它不显式让子类的原型等于父类的一个实例,而是创建了一个函数,在其内部实现这种继承。这种方式也称原型式继承。
//原型式继承,所有实例会共享属性的值
function object(o){
function F(){} //创建一个临时的构造函数
F.prototype=o; //将传入的参数作为该构造函数的原型
return new F(); //返回这个构造函数的一个实例
}
接着,它利用另一个函数以某种方式来增强object返回的那个对象,最终返回增强后的对象。
fucntion createAnother(original){
var clone=Object(original); //调用一个新对象
clone.sayHi=function(){ //增强对象
alert("Hi");
};
return clone;
}
来看一个寄生式继承的例子:
//原型式继承,所有实例会共享属性的值
function object(o){
function F(){} //创建一个临时的构造函数
F.prototype=o; //将传入的参数作为该构造函数的原型
return new F(); //返回这个构造函数的一个实例
}
function createAnother(original){
var clone=object(original); //调用一个新对象
clone.sayHi=function(){ //增强对象
alert("Hi");
};
return clone;
}
var person={
name:"Tom",
friends:["Lily"]
};
var anotherPerson=createAnother(person);
anotherPerson.sayHi(); //Hi
alert(anotherPerson.name); //Tom
上面的例子,anotherPerson继承了person的name和friends属性,并且还拥有自己的sayHi()方法。
寄生组合式继承模式
就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(subType,superType){
var prototype=Object(superType.prototype); //继承父类的原型
prototype.constructor=subType; //将构造函数指针指向子类,增强对象
subType.prototype=prototype; //将结果指定给子类原型
}
inheritPrototyoe()函数接受两个参数,分别为子类构造函数和父类构造函数。
在函数内部,第一步是创建了父类的一个副本;第二步是为创建的副本添加constructor属性,弥补重写原型而失去的默认的constructor属性值;第三步是,将新创建的这个父类的副本赋给子类的原型。
来看一个例子:
//所有实例会共享属性的值
function object(o){
function F(){} //创建一个临时的构造函数
F.prototype=o; //将传入的参数作为该构造函数的原型
return new F(); //返回这个构造函数的一个实例
}
function inheritPrototype(subType,superType){
var prototype=Object(superType.prototype); //继承父类原型
prototype.constructor=subType; //将构造函数指针指向子类,增强对象
subType.prototype=prototype; //将结果指定给子类原型
}
function Person(name){
this.name=name;
this.friends=["Lily"];
}
Person.prototype.sayName=function(){
return this.name;
}
function Child(name,age){
Person.call(this,name); //借用构造函数继承实例属性
this.age=age;
}
inheritPrototype(Child,Person);
Child.prototype.sayAge=function(){
return this.age;
}
var p1=new Child("Tom",10);
alert(p1.sayName()); //Tom
alert(p1.sayAge()); //10
alert(p1.friends); //Lily
var p2=new Child("Sam",5);
alert(p2.sayName()); //Sam
alert(p2.sayAge()); //5
alert(p2.friends); //Lily
p1.friends.push("Bob");
alert(p1.friends); //Lily,Bob
alert(p2.friends); //Lily
优点
寄生组合式继承,在整个实现的过程中只调用一次父类构造函数,避免了创建不必要的、多余属性。并且,能够保持原型链不变。