构造函数、原型对象和实例对象
在介绍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
构造函数,并初始化了name
、age
和friends
三个属性。接着创建了两个实例对象,分别为p1
和p2
。观察这个例子,每个属性都是为各自所拥有的,并不会相互影响。这就是因为每个实例对象都拥有一份属性的副本。
每个实例对象都有一个属性指向它的构造函数,这属性就是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
类,它拥有两个属性(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()
语句之后。 - 用原型链来实现继承的方法中,不能使用对象字面量来创建子类原型的方法或属性。因为这样会重写了原型,切断了它与父类的联系,也就不存在继承关系了。
看一个例子,来验证注意项中的第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
属性的值时,实际上修改的是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返回的那个对象,最终返回增强后的对象。
//原型式继承,所有实例会共享属性的值
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
优点
寄生组合式继承,在整个实现的过程中只调用一次父类构造函数,避免了创建不必要的、多余属性。并且,能够保持原型链不变。