Javascript 的继承机制
1. Javascript对象
首先看看Javascript中的对象是什么。从概念上看,Javascript中对象与其他强类型的语言,如C++ / Java,基本类似,由一系列属性和方法构成。不过Javascript是弱类型的,对象的创建形式更加灵活。主要有Object literal(文本对象)的方式,如
var rect = {width:100, height:200};
这行代码创建了一个对象,这个对象有两个属性,并设置了初始值。
另外一种创建对象的方式是用关键字new,具体见第二小节
Javascript中的类型大致可分为两大类:
l 原子类型,包括number, Boolean, null, string 和undefined;
l 对象类型,就是这里要讨论的对象
关于对象比较简单,就说这么多,可以参考<Javascript – The Definitive Gide>或者其他网上的文章。
2. Javascript类
接着再来看看类,严格来说Javascript并没有类,是用对象来表示类的。下面让我们来通过一个简单的具体例子来看看Javascript是如何通过对象来表示类的。这里借用<Javascript – The Definitive Gide>一书中的例子Rectangle,代码如下:
function Rectangle(w, h) {
this.width = w;
this.height = h;
}
这里实际上仅仅是定义了一个函数,这个函数将参数值赋值给this的作用范围中的两个变量(作用范围请参考后续文章,或者其他网上的资源或者书籍),如下面的代码示例:
<html>
<head>
<title>Javascript inheritance testing</title>
</head>
<body>
<h2>Javascript inheritance testing</h2>
<script>
var width;
var Height;
function Rectangle(w,h){
this.width = w;
this.height = h;
}
document.write("width = " + width + ", height = " + height + "<br>");
Rectangle(100,200);
document.write("width = " + width + ", height = " + height + "<br>");
</script>
</body>
</html>
在调用了函数Rectangle(100,200)后,全局范围的两个变量width, Height被赋予了函数的参数的值,当没有与其他对象关联时,this默认指全局。但是当这样的一个函数与Javascript关键字new 配合使用,就有了新的含义,看代码
var rect = new Rectangle(100, 200);
这行代码做了以下几件事:
l new创建了一个Object对象,这是Javascript最简单的对象,
l 将这个对象传递给函数调用Rectangle(100, 200),也就是相当于限定这个函数中this指向这个新建的对象,于是这个对象就有了widht和height属性,并分别被赋值为100和200.
l 为这个对象增加一个内置的constructor属性,指向这个函数,后文有详述
l 然后将这个对象赋值给rect变量。
通过这几件事分析,我们可以发现,这似乎与Java或者C++创建某个类的对象的方式是一样的。于是我们可以将这样的代码模式定义为Javascript的类机制,即:
通过定义一个含有this的特殊的函数,来初始化一个对象的属性,这个函数被称为constructor—构造函数,通常也把这个函数的定义看做是类定义。按Javascript的规范,这样的函数通常用名词来命名,表示一种类型,如本文中的例子Rectangle。我们知道Javascript中的函数,其实也是对象,这点通过 Rectangle instanceof Object == true可以证明。所以如前面提到的,Javascript的类机制实际上是用对象来实现的。
3. Prototype
Javacript的函数都有个prototype属性,定义函数的时候,Javascript就为这个函数(注意,一个函数就是一个对象)自动设置了prototype为一个对象,这个对象有个属性constructor指向这个函数。
而用构造函数创建对象的时候,这个对象会有个内置的属性constructor指向构造函数,所以通过由某个构造函数创建的多个对象,都指向同一个构造函数(函数也是对象),而这个构造函数有个prototype属性。
例,Rectangle.prototype.constructor 等于 Rectangle.
var rect1 = new Rectangle(1,1);
var rect2 = new Rectangle(1,2);
var rect3 = new Rectangle(1,3);…
这样创建的rect1, rect2, rect3都有constrcutor属性,而且
rect1.constructor, rect3.constructor, rect3.constructor都等于Rectangle
有了这样的机制,我们就可以通过扩充构造函数的prototype来定义该类的公共的东西了,比如方法。例,我们可以为Rectangle加个area方法
Rectangle.prototype.area = function() {
return this.wdith * this.height;
};
这样我们的对象rect1就可以这样调用area方法来计算面积了:
rect1.constructor.prototype.area();
不过这样调用写法是很麻烦的,我们可以直接rect1.area()来调用。Javascript内置了属性和方法的查找机制,首先看rect1对象本身是否有area方法,这里是没有的,然后Javascript会看这个对象的constructor.prototype对象是否有这个area方法,于是找到了方法定义,就可以执行调用了,如果在这个对象的constructor.prototype中没有,Javascript会看constructor.prototype是否还有constructor.prototype属性,如果有的话,会继续这样的查找,直到找到,或者达到Object.prototype,这就是所谓的prototype chain。
4. 继承
Javascript中,每个函数(函数也是对象)都有两个方法,call和apply。这两个方法功能是一样的,就是函数本省的功能,所不同的是用传入的对象作为函数中的this的指向的对象。
还是看Rectangle例子
var width =50;
var Height = 60;
//定义Rectangle类
function Rectangle(w,h){
this.width = w;
this.Height = h;
}
//创建一个Rectangle对象,width = 100, height = 200
var rect = new Rectangle(100,200);
//将rect传入,设定Rectangle中this指向rect
Rectangle.call(rect, 150, 250);
//通过打印rect的width, Height,我们发现值改变了
document.write("width = " + rect.width + ", height = " + rect.Height + "<br>");
//将全局this传入,设定Rectangle中this指向全局
Rectangle.call(this, 155, 255);
//通过打印rect的width, Height,我们全局的两个变量值改变了
document.write("width = " + width + ", height = " + Height + "<br>");
有了Rectangle类之后,又有需要有颜色的Rectangle,用面向对象的思想,当然是从Rectangle继承一个新的类是自然的方式。这行借用函数对象的call方法来实现类似的调用父类构造函数,代码如下
function ColoredRectangle(w,h,c){
//将this传入作为Rectangle中this指向的对象,与Java中调用父类的构造函数很相似
Rectangle.call(this, w, h);
//设置ColoredRectangle自有属性
this.color = c;
}
通过这种复用代码的方式,我们实现了继承父类的属性了,如何继承父类的公用方法呢?还记得前面讲的prototype吗?
ColoredRectangle.prototype = Rectangle.prototype
这样不就是可以共享父类中公共的资源了吗?可以测试,确实ColoredRectangle有了Area方法,似乎我们实现了继承方法的目标。
我们再来为ColoredRectangle扩充一个方法getColor
ColoredRectangle.prototype.getColor = function(){
return this.color;
}
一切都似乎很完美,但是这个时候我们看看Rectangle,我们会发现Rectangle也有了getColor方法了,这可不是我们想要的,测试代码如下
//定义Rectangle类
function Rectangle(w,h){
this.width = w;
this.Height = h;
}
//为类增加Area方法
Rectangle.prototype.Area = function(){
return this.width * this.Height;
}
var rect = new Rectangle();
function ColoredRectangle(w,h,c){
//将this传入作为Rectangle中this指向的对象,与Java中调用父类的构造函数很相似
Rectangle.call(this, w, h);
//设置ColoredRectangle自有属性
this.color = c;
}
ColoredRectangle.prototype = Rectangle.prototype;
ColoredRectangle.prototype.getColor = function(){
return this.color;
}
document.write(("getColor" in rect) + "<br>");
为什么呢?问题出在这里ColoredRectangle.prototype = Rectangle.prototype,这行代码将父类的prototype赋值给子类了,也就是说子类和父类的prototype指向了同一个对象,所以子类增加方法,父类也就有了。还记得前面说的prototype.constructor.prototype吧,我们可以再子类和父类的prototype中间在加一个对象,这样就实现了父类和子类prototype的分离了。
将ColoredRectangle.prototype = Rectangle.prototype改为
ColoredRectangle.prototype = new Rectangle();
这样ColoredRectangle.prototype.constructor.prototype指向了Rectangle.prototype,通过Javascript的prototype chain的查找机制,就可以共享父类的公共资源了。
但是这种方式有带来了点小问题,ColoredRectangle.prototype指向一个Rectangle对象,那么ColoredRectangle.prototype就有了width和Height属性,而ColoredRectangle本身已经有了这两个属性,虽然不会有啥逻辑问题,但是带来了内存的浪费。
可以用Javascript的delete来删除这两多余的属性
delete ColoredRectangle.prototype.width;
delete ColoredRectangle.prototype.Height;
到这里我们的继承目标就基本实现了,但是如果父类有很多属性的话,要一个一个删除似乎也是比较阀门的事情,这里可以通过一个小技巧来解决,用一个没有属性的对象来代替
ColoredRectangle.prototype = new Rectangle();中的new Rectangle, 并把Rectangle.prototype赋给这个对象,这样就不用删除属性了,代码如下
//定义Rectangle类
function Rectangle(w,h){
this.width = w;
this.Height = h;
}
//为类增加Area方法
Rectangle.prototype.Area = function(){
return this.width * this.Height;
}
var rect = new Rectangle();
function ColoredRectangle(w,h,c){
//将this传入作为Rectangle中this指向的对象,与Java中调用父类的构造函数很相似
Rectangle.call(this, w, h);
//设置ColoredRectangle自有属性
this.color = c;
}
function Empty(){};
Empty.prototype = Rectangle.prototype;
ColoredRectangle.prototype = new Empty();
ColoredRectangle.prototype.getColor = function(){
return this.color;
}
var crect = new ColoredRectangle(2,3,"red");
document.write(("getColor" in rect) + "<br>");
document.write(("getColor" in crect) + "<br>");
到这里我们的继承目标才算是比较完美的实现了。