木子屋 Dnawo's BLOG

面向对象的JavaScript

👤 dnawo 📅 2007-11-22 👁 1722 👍 0 💬 0 🔄 《JavaScript权威指南》 📂 脚本语言
虽然JavaScript支持我们称为对象的数据类型,但是它并没有正式的类的概念。这使它有别于那些经典的面向对象程序设计语言,如C++和Java等。在面向对象的程序设计语言中,共有的概念是强类型和支持以类为基础的继承机制。根据这个评判标准,很容易就能将JavaScript从真正的面向对象语言中区分出来。但是,我们又会发现JavaScript大量的使用了对象,而且它还有自己的以原型对象为基础的继承机制,这些又说明JavaScript是一种真正面向对象语言。它从大量其他的(相对比较模糊)面向对象语言中获取了灵感,这些语言采取以原型对象为基础的继承机制,而不是采用以类为基础的继承机制。

虽然JavaScript不是一种以类为基础的面向对象程序设计语言,但是它在模拟像Java和C++这样的以类为基础的语言时是相当出色的。在本章中,我们已经使用了类这个术语。本节将正式地探讨JavaScript和Java及C++这样采用真正的以类为基础的继承机制的语言之间的共通之处[1]。

首先,我们来定义几个术语。我们已经知道,对象是一个数据结构,包含各种命名了的数据段,而且还能包含对这些数据段进行操作的各种方法。一个对象组将数值和方法组织到一个方便的包中,这样就增强了代码的模块性和可再用性,从而使程序设计更加容易。JavaScript中的对象可以具有大量的属性,而且还可以动态地将这些属性添加到对象中。这在像Java和C++这样的强类型语言中是做不到的。在那些语言中,每个对象都有一套预定义的属性[2],而且每个属性都有预定义的类型。在使用JavaScript的对象来模拟面向对象的程序设计方法时,通常是给每个对象预先定义一套属性以及这些属性的数据类型。

在Java和C++中,类定义了对象的结构。类确定了一个对象包含有哪些域以及每个域包含什么样的数据类型。类也定义了对对象进行操作的方法。虽然JavaScript并没有正式的类的概念,但是我们已经见到过,它用构造函数和原型对象模拟了类。

在JavaScript和以类为基础的面向对象语言中,同一类可以具有多个对象。我们常常称对象是它所属的那个类的实例,所以任何类都可以有多个实例。有时我们使用实例化(instantiate)这个术语来描述创建一个对象(类的一个实例)的过程。

在Java中有一条常用的约定,即命名类时以大写字母开头,命名对象时以小写字母开头。这一约定可以帮助我们区分代码中的类和对象,JavaScript没用了这一有用的约定。例如,在前面几节中,我们定义了类Circle和类Rectangle,而且分别创建了这两个类的实例,名c和rect。

一个Java类的成员有四种可能的基本类型:实例属性、实例方法、类属性以及类方法。在接下来几节中,我们将研究这些成员类型之间的区别,并且说明在JavaScript中如何模拟它们。

8.5.1 实例属性

每个对象都有它自己单独的实例属性的副本。简而言之,如果有10个给定的类的对象,那么每个实例属性就有十个副本。例如,在Circle类中,每个Circle对象都有一个属性r,它指定了这个圆的半径,这里r就是一个实例属性。由于每个对象都有它自己的实例属性副本,所以通过单独的对象可以存在这些变量。例如,如果对象c是Circle类的一个实例,可以这样来引用半径:

c.r

在默认情况下,JavaScript中的任何对象属性都是实例属性。但是为了真实地模拟面向对象的程序设计语言,我们说JavaScript中的实例属性是那些在对象中用构造函数创建的或初始化的属性。

8.5.2 实例方法

实例方法和实例属性非常相似,只不过它是方法而不是数值(在Java中,函数和方法并不像它们在JavaScript中那样是数据,所以这一区别就更加明显了)。实例方法是由特定对象或实例调用的。Circle类中的方法area()就是一个实例方法。它是由Circle的对象c调用的:

a = c.area();

实例方法使用了关键字this来引用它们要操作的对象或实例。虽然一个类的任何实例都可以调用实例方法,但是这并不意味着每个对象都像实例属性那样含有自己专有的方法副本。相反,每个实例方法都是由类的所有实例共享的。在JavaScript中,给类定义一个实例方法,是通过把构造函数的原型对象中的一个属性设置为函数值来实现的。这样,由那个构造函数创建的所有对象都会共享一个已继承的对函数的引用,而且使用上面所述的方法调用语法就能够调用这个函数。

8.5.3 类属性

在Java中,类属性是一个与类相关联的变量,而不是和类的每个实例相关联的变量。无论一个类创建多少个实例,每个类属性都只有一个副本。就像实例属性是通过类的实例存取一样,类属性是通过类存取的。在JavaScript中,Number.MAX_VALUE就是类属性的一个例子,因为属性MAX_VALUE就是通过类Number来存取的。由于每个类属性都只有一个副本,所以本质上说类属性是全局变量。但是它们与一个类关联在一起,在JavaScript的名字空间中拥有一个逻辑位置,这样它们就不会被其他的同名属性覆盖。为了尽可能说得清楚,我们模拟了一个JavaScript的类属性,简单地定义了构造函数自身的一个属性。例如,要创建一个类属性Circle.PI来存储数学常量pi,我们可以采取如下方法:

Circle.PI = 3.14;

虽然Circle是一个构造函数,但是由于JavaScript函数是对象,所以我们可以创建函数的属性,就像创建对象的属性一样。

8.5.4 类方法

最后,我们介绍一下类方法。类方法是一个与类关联在一起的方法,而不是和类的实例关联在一起的方法。要调用类方法,就必须使用类本身,而不能使用该类的特定实例。方法Date.parse()就是一个类方法。通过构造函数对象Date才能调用这个方法,而不能通过Date类的一个特殊实例来调用它。

由于类方法不能通过一个特定对象调用,所以使用关键字this对它来说没有意义(this引用的是调用实例方法的对象)。和类属性一样,类方法是全局性的。因为类方法不是对特定对象进行操作的,所以类方法更容易被认为是由类调用的函数。同样的,将这些函数关联到一个类可以使它们在JavaScript的名字空间中有一个便利的位置,并且防止出现命名冲突。在JavaScript中,要定义一个类方法,只需要用合适的函数作为构造函数的属性即可。

8.5.5 例子:类Circle

8.5.5:Circle的实例属性和类属性及实例方法和类方法的定义

function Circle(radius)
{ 
//这个构造函数定义了类自身。
//r是构造函数定义并初始化的一个实例属性。
this.r = radius;
}

//Circle.PI是一个类属性,它是构造函数的一个属性。
Circle.PI = 3.14159;

//以下的函数将计算圆的面积。
function Circle_area(){return Circle.PI * this.r * this.r;}

//下面我们通过把函数赋给构造函数的原型对象使它成为一个实例的方法。
//记住,在JavaScript1.2中,我们可以用函数直接量定义一个无需命名为Circle_area的函数。
Circle.prototype.area = Circle_area;

//以下是另一个函数,它以两个Circle对象作为实际参数,并
//返回其中较大的一个(取半径较大的实际参数)
function Circle_max(a,b)
{
	if(a.r > b.r)return a;
	else return b;
}

//由于该函数比较的是两个Circle对象,所以将它看作对个别Circle对象进行操作的实例方法,
//是没有意义的。但是我们不希望它成为一个独立的函数,所以我们把它赋予一个构造函数,
//使它成为类方法。
Circle.max = Circle_max;

//下面的代码使用了Circle对象的各个域:
var c = new Circle(1.0);      //创建Circle类的一个实例。
c.r = 2.2;                    //设置实例属性r。
var a = c.area();             //调用实例方法area()。
var x = Math.exp(Circle.PI);  //在我们自己的计算中使用属性PI。
var d = new Circle(1.2);      //创建另一个Circle实例。
var bigger = Circle.max(c,d); //使用类方法max()。

8.5.6 超类和子类

在Java、C++和其他以类为基础的面向对象语言中,都有一个明确的类层次的概念。每个类都有一个超类,它们从超类中继承属性和方法。类还可以被扩展,或者说子类化,这样其他子类就能够继承它的行为。我们已经知道,JavaScript支持的是以原型为基础的继承机制,而不是以类为基础的继承机制,但是我们仍旧能够总结出类似的类层次图。在JavaScript中,类Object是最通用的类,其他所有类都是专用化了的版本,或者说是Object的子类。另一种解释方法是Object是所有内部类的超类。所有类都继承了Object的基本方法。

我们已经学会了对象如何从它们的构造函数的原型对象中继承属性。那么它们又是如何继承类Object的属性的呢?我们知道,原型对象本身就是一个对象,它是由构造函数Object()创建的。这就意味着原型对象继承了Object.prototype属性。因此类Complex对象就继承了Complex.prototype对象的属性,而后者又继承了Object.prototype的属性。由此可以推出,对象Complex继承了两个对象的属性。在Complex对象中查询某个属性时,首先查询的是这个对象本身。如果在这个对象中没有发现要查询的属性,就查询Complex.prototype对象。最后,如果在那个对象中还没有找到要查询的属性,就查询Object.prototype对象。

注意,由于Complex原型对象是在查询Object的原型对象之前被查询的,所以Complex.prototype的属性就隐藏了Object.prototype的同名属性。例如,在例8-6的类定义中,我们在Complex.prototype对象中定义了toString()方法,虽然Object.prototype中也定义了同名的方法,但是对象Complex永远也不会看到它,因为首先发现的是Complex.prototype中定义的toString()。

我们在本章中使用的类都是Object的直接子类。这是JavaScript的程序设计所特有的,通常它不需要生成更复杂的层次。但在必要的情况下,有可能对其他类进行子类化。例如,假定我们要生成类Complex的一个子类,以便能添加一些新方法。要做到这一点,只需要确保新类的原型对象是Complex的一个实例,这样它就能继承Complex.prototype的所有属性:

//下面是子类的构造函数
function MoreComplex(real,imaginary)
{
	this.x = real;
	this.y = imaginary;
}

//我们将它的原型对象作为Complex对象。
//这意味着新类的实例将继承MoreComplex.prototype,
//后者由Complex.prototype继承而来。
//Complex.prototype又由Object.prototype继承而来。
MoreComplex.prototype = new Complex(0,0);

//下面给子类添加一个新方法或新特性。
MoreComplex.prototype.swap = function(){
	var tmp = this.x;
	this.x = this.y;
	this.y = tmp;
}

上面展示的子类化方法有一点缺陷。由于我们明确地把MoreComplex.prototype设成了我们所创建的一个对象,所以就覆盖了JavaScript提供的原型对象,而且抛弃了给定的constructor属性。该属性引用的是创建这个对象的构造函数。但是MoreComplex对象继承了它的超类的constructor属性,它自己并没有这个属性。明确地设置这一属性可以解决这一问题:

MoreComplex.prototype.constructor = MoreComplex;

但是要注意,在JavaScript1.1中,constructor属性是只读的,不能用这种方式设置它。

附注:

[1] 即使你不熟悉这些语言和面向对象的程序设计方法,也应该阅读本节的内容。
[2] 在Java和C++中,它们被称为字段(field),但是在这里我们将它称之为属性,因为这是JavaScript的术语。

评论(0)

暂无评论。

计算题
评论需审核通过后显示
← 上一篇 🔒 密码保护 下一篇 → 新云网站管理系统 v3.1.0.1122下载信息页增加"发送到手…