在介绍javascript继承之前,必须先介绍下一些相关的基本概念。

大家都知道,javascript中其实并没有类的概念。但是,用构造函数跟原型对象却可以模拟类的实现。那么什么是构造函数,什么又是原型对象呢?接下来我们将做详细解释。

虽然javascript没有类,但是,在这里,我们先用“类”来进行称呼,以方便说明问题。

构造函数和实例对象

其实构造函数也就是一个函数,只不过它于普通的函数又有点不同:

  • 没有显示的创建对象;
  • 直接将属性和方法赋给this;
  • 没有return语句;

构造函数是用来构造新对象的。可以用new关键词来调用构造函数,以创建特定类型的新对象。如,创建一个Object类型的对象实例:

var o=new Object();

为了区别构造函数和普通函数,通常规定构造函数的命名首字母大写,而普通函数的命名首字母小写。当然,这不是必须的,但是,是一个很好的习惯。

通过用构造函数创建并初始化的属性是实例属性。

所谓的实例属性就是指,通过该构造函数创建的每个对象,都将拥有一份实例属性的单独拷贝。这些属性都是通过实例来访问的,值根据每个实例所定义的为准,若实例中没有定义,则为构造函数初始化时的默认值。来看一个例子:

function Person(name,age){  
    this.name=name;  
    this.age=age;  
}  
  
var p1=new Person("Lily",20);  
var p2=new Person("Sam",30);  
   
alert(p1.constructor==Person); //true  
alert(p2.constructor==Person); //true

上面的例子定义了一个Person构造函数,并初始化了nameagefriends三个属性。接着创建了两个实例对象,分别为p1p2。观察这个例子,每个属性都是为各自所拥有的,并不会相互影响。这就是因为每个实例对象都拥有一份属性的副本。

每个实例对象都有一个属性指向它的构造函数,这属性就是constructor

function Person(name,age){  
   this.name=name;  
   this.age=age;  
   this.friends=["Tom","Boo"];  
}  
  
var p1=new Person("Lily",20);  
var p2=new Person("Sam",30);  
   
alert(p1.name);  //Lily  
alert(p2.name);  //Sam  
p1.friends.push("Susan");  
alert(p1.friends);  //Tom,Boo,Susan  
alert(p2.friends);  //Tom,Boo

原型对象和实例对象

在javascript中,每个对象都有一个与之相关联的对象,那就是它的原型对象。类的所有实例对象都从它的原型对象上继承属性。

原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。

构造函数拥有一个prototype属性,指向原型。换句话来说,一个对象的原型就是它的构造函数的prototype属性的值。当一个函数被定义的时候,它会自动创建和初始化prototype值,它是一个对象,这时这个对象只有一个属性,那就是constructor,它指回和原型相关联的那个构造函数。

看个例子:

function Person(name,age){  
   this.name=name;  
   this.age=age;  
}  
  
alert(Person.prototype); //[object Object]  
alert(Person.prototype.constructor==Person); //true

也可以通过原型来创建属性和方法。通过原型创建的属性和方法是被所有实例所共享的。即,在一个实例中修改了该属性或方法的值,那么所有其他实例的属性或方法值都会受到影响:

function Person(name,age){  
   this.name=name;  
   this.age=age;  
}  
  
Person.prototype.friends=["Tom","Sam"];  
  
var p1=new Person("Lily",24);  
var p2=new Person("Susan",20);  
  
alert(p1.friends); //Tom,Sam  
alert(p2.friends); //Tom,Sam  
  
p1.friends.push("Bill");  
alert(p1.friends); //Tom,Sam,Bill  
alert(p2.friends); //Tom,Sam,Bill

由上面的例子可以看出,用原型定义的属性是被所有实例共享的。为p1添加了一个朋友,导致p2也添加了这个朋友。其实,很多情况下,这种现象并不是我们想看到的。那么什么时候应该用构造函数初始化属性和方法,哪些时候又该由原型对象来定义呢?

通常建议在构造函数内定义一般成员,即它的值在每个实例中都将不同,尤其是对象或数组形式的值;而在原型对象中则定义一些所有实例所共享的属性,即在所有实例中,它的值可以是相同的属性。

当用构造函数创建一个实例时,实例的内部也包含了一个指针,指向构造函数的原型对象。一些浏览器中,支持一个属性__proto__来表示这个内部指针:

function Person(name,age){  
   this.name=name;  
   this.age=age;  
}  
  
Person.prototype.sayName=function(){  
   alert(this.name);  
}  
  
var p1=new Person("Lily",24);  
  
alert(p1.__proto__.sayName); //function (){alert(this.name);}  
  
alert(p1.__proto__.constructor==Person);  //true

从前面介绍原型对象于实例对象及构造函数的关系中,我们已经知道,给原型对象添加属性和方法只要像这样定义即可:Person.prototype=name

那么是否每定义一个Person的属性,就要敲一遍Person.prototype呢?答案是否定的,我们也可以像用对象字面量创建对象那样来创建原型对象:

function Person(){  
}  
  
Person.prototype={  
   name:"Tom",  
   age:29  
}  
  
var p1=new Person();  
  
alert(p1.name); //Tom  
alert(p1.age);  //29

有一点要注意,这个方法相当于重写了整个原型对象,因此切断了它与构造函数的关系,此时Person.prototype.constructor不再指向Person

function Person(){  
}  
  
Person.prototype={  
   name:"Tom",  
   age:29  
}  
  
var p1=new Person();  
  
alert(Person.prototype.constructor==Person); //false  
alert(Person.prototype.constructor==Object); //true

因此,如果想要让它重新指向Person,可以显示的进行赋值:

function Person(){  
}  
  
Person.prototype={  
  constructor:Person,  
  name:"Tom",  
  age:29  
}  
  
var p1=new Person();  
  
alert(Person.prototype.constructor==Person); //true  
alert(Person.prototype.constructor==Object); //false

原理

原型链实现继承的基本思路:利用原型让一个引用类型继承另一个引用类型的属性和方法。

那么怎么做到这种继承呢?方法很简单,就是让子类的原型等于父类的一个实例。这样一来,子类的原型中就拥有一个指向父类原型的内部指针,自然就继承了它的方法和属性。

来看一个例子:

//定义一个人的类  
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类,它拥有两个属性(nameage)和一个方法(sayName())。然后定义了一个Child类,它暂时不拥有属性和方法。接着,将Childprototype指向Person类的一个实例,这就实现了继承。在这之后,为Child的原型添加了方法。

alert语句中可看出,Child的实例p1,继承了Person的属性和方法。

其实,在上面的例子中,还忽略了一个环,那就是Person继承了Object,这个继承也是通过原型链实现的。这样一来,上面那个例子的继承关系可用下图来表示:

其实上图所展示的也就是一条原型链。(这里省略了构造函数的指针关系。因为通过原型的指向也可以得到构造函数的指向,所以为了简洁,这里就不在指出。)

用文字,原型链也可以这样表示的:

/* 
 *原型链: 
 *p1 [Child的实例] 
 *   Child.prototype [Person的实例] 
 *      Person.prototype 
 *         {sayName:...} 
 *         Object.prototype 
 *            {toString:...} 
 */

注意:

  1. 给子类原型添加方法或属性一定要放在替换原型的语句之后。在上面的例子中,就是要在Child.prototype=new Person()语句之后。
  2. 用原型链来实现继承的方法中,不能使用对象字面量来创建子类原型的方法或属性。因为这样会重写了原型,切断了它与父类的联系,也就不存在继承关系了。

看一个例子,来验证注意项中的第2点:

//定义一个人的类  
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属性的值时,实际上修改的是Personfriends属性的副本,不会影响到其他实例。这么说来,借用构造函数实现继承就有一下两个有点:

  • 可以实现在子类构造函数中向父类构造函数传递参数;
  • 子类的每个实例中都拥有父类属性的一个副本,即实例属性相对独立,不会互相影响。

缺点

借用构造函数实现继承也存在问题,就是方法都必须在构造函数中定义,原因在于父类原型中定义的方法,在子类中是不可见的。这样一来,函数的复用就无从谈起了。

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会得到两个实例属性(namefriends),它们位于Child的原型中,是被所有Child实例所共享的;第二次是在创建Child实例时,调用了Child构造函数,它的内部就也调用了Person,这时Child的每个实例都拥有了Person中属性的副本,于是,这两个属性(namefriends)就覆盖了原型中的两个同名属性。等于说,每一次在调用Child构造函数时,都会产生两组namefriends属性:一组在Child原型中,一组在实例上。造成资源浪费。

为了避免组合继承的确定,就有了寄生组合式继承的方法。

原理

寄生组合式继承的基本思路是:通过借用构造函数来继承属性,通过原型链混成形式来继承方法。本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型。

寄生式继承

在讲寄生组合式继承前,先来了解下寄生式继承。

所谓寄生式继承,就是创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回该对象。

首先,它会借用一个函数来实现类似原型链的继承,只不过它不显式让子类的原型等于父类的一个实例,而是创建了一个函数,在其内部实现这种继承。这种方式也称原型式继承。

    //原型式继承,所有实例会共享属性的值  
    function object(o){  
        function F(){}  //创建一个临时的构造函数  
        F.prototype=o;  //将传入的参数作为该构造函数的原型  
        return new F(); //返回这个构造函数的一个实例  
    }  

接着,它利用另一个函数以某种方式来增强object返回的那个对象,最终返回增强后的对象。

//原型式继承,所有实例会共享属性的值  
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  

优点

寄生组合式继承,在整个实现的过程中只调用一次父类构造函数,避免了创建不必要的、多余属性。并且,能够保持原型链不变。