木子屋 Dnawo's BLOG

JavaScript的原型对象和继承

👤 dnawo 📅 2007-11-22 👁 1741 👍 0 💬 0 🔄 《JavaScript》权威指南 📂 脚本语言
用构造函数把方法赋予它要初始化的对象,效率非常低。如果我们这样做,那么构造函数创建的每一个对象都会有相同的方法属性的副本。有一种更为有效的方式可以用来声明方法、常量以及其他能被类的所有对象共享的属性。

JavaScript对象都有"继承"原型对象的属性[1]。每个对象都有原型对象,原型对象的所有属性是以它为原型对象的属性。也就是说,每个对象都继承原型对象的所有属性。

一个对象的原型是由创建并初始化该对象的构造函数定义的。JavaScript中的所有函数都有prototype属性,它引用了一个对象。虽然原型对象初始化时是空的,但是你在其中定义的任何属性都会被该构造函数创建的所有对象继承。

构造函数定义了对象的类,并初始化了类中状态变量的属性,如width和height。因为原型对象和构造函数关联在一起,所以类的每个成员都从原型对象继承了的属性。这说明原型对象是存放方法和其他常量属性的理想场所。

注意,继承是在查询一个属性值时自动发生的。属性并非从原型对象复制到新的对象的,它们只不过看起来像是那些对象的属性。这其中有两点重要的含义:一是使用原型对象可以大量减少每个对象对内在的需求量,因为对象可以继承许多属性。二是即使属性在对象被创建之后才添加到它的原型对象中,对象也能够继承这些属性。

每个类都有一个原型对象,这个原型对象都具有一套属性。但是实际上却有大量类实例,每个实例都能继承原型对象的属性。由于一个原型对象能够被多个对象继承,所以JavaScript必须强化读写属性之间的不对称性。在读对象o的属性p时,JavaScript首先检查o是否具有一个名为p的属性,如果o没有这个属性,JavaScript就会再检查o的原型对象是否具有这个属性。这样才使得以原型为基础的继承机制起作用。

而当你写一个属性的值时,JavaScript并不使用原型对象。要明白为什么,就考虑一下如果这样做会出现什么样的后果。假设你在对象o还没有一个名为p的属性时要设置属性o.p的值,进一步假设JavaScript继续在o的原型对象中查询属性p,并且允许你设置原型对象的这一属性,那就改变了整个对象类的p值,而不是仅仅改变了自己想要改变的属性值。

因此,属性的继承只发生在读属性值时,而在写属性值时不会发生。如果你设置的对象o的属性p是对象o从它的原型对象继承而来的,那么结果是你直接在对象o中创建了一个新属性p。现在o已经有了自己的名为p的属性,它就不会再从它的原型对象继承p的值了。当你读p的值时,JavaScript首先查询o的属性。由于它发现了o中定义的p,它就不必再查询原型对象,也就不会再发现原型对象中定义的p值了。我们有时说o中的p"遮蔽"了或者说"隐藏"了原型对象中的属性p。原型对象的继承是一个容易让人混淆的主题。

因为原型对象的属性被一个类的所有对象共享,所以通常只用它们来定义类中所有对象的相同的属性。这使用原型对象适用于方法定义。另外原型属性还适合于那些具有常量值(如数学常量)的属性的定义。如果你在类中定义了一个属性,它有一个非常常用的默认值,那么你可以在原型对象中定义这个属性和它的默认值。然后,那些不想使用的默认值的少数对象可以创建自己专有的、非共享的属性副本,并且将它定义为自己的、非默认的值。

让我们从对原型对象继承机制的抽象讨论转移到一个具体的例子上来。假设我们定义了一个构造函数Circle(),用来创建表示圆形的对象。因为这个类的原型对象是Circle.prototype[2],所以我们可以定义一个对所有Circle对象都有效的常量。

Circle.prototype.pi = 3.14159;

例8-4展示的Circle没有任何问题。这段代码定义Circle类时,首先定义了一个构造函数Circle()来初始化每个对象,然后通过设置Circle.prototype属性来定义所有的类实例共享的方法和常量。

例8-4:用原型对象定义一个Circle类

//为我们的类定义一个构造函数方法。
//用它初始化Circle对象的属性。
//为每个Circle对象的属性值都不同。
function Circle(x,y,r)
{
	this.x = x; //圆心的x坐标。
	this.y = y; //圆心的y坐标。
	this.r = r; //圆的半径。
}

//创建并舍弃初始的Circle对象。
//在JavaScript 1.1中,这将创建原型对象。
new Circle(0,0,0)

//定义一个常量,即所有Circle对象共享的属性。
//事实上,我们只是使用了Math.PI,
//不过这样做只是为了满足本例的需要。
Circle.prototype.pi = 3.14159;

//定义一个计算圆周长的方法。
//首先声明一个函数,然后把它赋给原型对象的一个属性。
//注意上面定义的常量的用法。
function Circle_circumference(){return 2 * this.pi * this.r;}
Circle.prototype.circumference = Circle_circumference;

//定义另一个方法。这次使用函数直接量来定义函数,
//并且只用一步就将它赋给原型对象的属性
Circle.prototype.area = function(){return this.pi * this.r * this.r;}

//Circle类已经定义了。
//现在我们可以创建一个实例并调用它的方法。
var c = new Circle(0.0,0.0,1.0);
var a = c.area();
var p = c.circumference();

不只是用户定义的类具有原型对象,像String和Date这样的内部类同样具有原型对象,你也可以给它们赋值[3]。例如,下面的代码定义了一个新方法,它适用于所有String对象:

//如果最后一个字符是c,返回true。
String.prototype.endsWith = function(c)
{
	return (c == this.charAt(this.length-1));
}

在String的原型对象中定义了新方法endsWith()后,可以用如下方式来使用它:

var message = "hello world";
message.endsWith("h"); //返回false
message.endsWith("d"); //返回true

附注:

[1]原型是在JavaScript1.1中引入的,已经被废弃的JavaScript1.0不支持它。
[2]构造函数的原型对象是由JavaScript自动创建的。在大多数JavaScript版本中,它都会自动给予每个函数一个空的原型对象,以防它被用作一个构造函数。但是在JavaScript1.1中,在函数首次被用作构造函数前,不会为它创建原型对象。这就是说,为了能和JavaScript1.1兼容,在使用原型对象给那个类的对象赋予方法和常量之前,至少应该创建该类的一个对象。因此,如果我们已经定义了构造函数Circle(),但是还没有来创建过任何Circle对象,那么我们应该定义如下常量属性pi:
//First create and discard a dummy object;forces prototype object creation.
nwe Circle();
//Now we can set properties in the prototype.
Circle.prototype.pi = 3.14159;
[3]只适用于JavaScript1.1及其后的版本。

评论(0)

暂无评论。

计算题
评论需审核通过后显示
← 上一篇 🔒 密码保护 下一篇 → 🔒 密码保护