alt.lang.jre: 使用 Rhino
JavaScript 是一门众所周知的语言,它可以动态操作和访问 Web 页面的内容。采用 Rhino(JavaScript 的 100% 纯 Java 实现)之后,很多 Java 开发人员发现,JavaScript 也是一种快速构建和部署基于 GUI 的应用程序的优秀工具。本文是 alt.lang.jre系列的第 5 部分,在文中,developerWorks 的撰稿人 Michael Squillace 将向您简要介绍 Rhino 的基础知识,它是 Java 平台上的一种基于原型的 Java 语言的可替代品。
Rhino 是一种使用 Java 语言编写的 JavaScript 的开源实现。与本系列的其他很多语言一样,Rhino 是一种动态类型的、基于对象的脚本语言,它可以简单地访问各种 Java 类库。Rhino 从 JavaScript 中借用了很多语法,让程序员可以快速编写功能强大的程序。最为明显的区别是,Rhino 不再使用语句结束符( ; ),放宽了变量声明规则,并且极大地简化了修改和检索对象属性的语法(没有求助于调用存取方法)。
由 于 Rhino 是 JavaScript 的一种基于 Java 的实现,所以对于 Java 开发人员来说,它应该特别易于使用。JavaScript 的(以及 Rhino 的)语法非常类似于 Java 编程语言。这两种语言都采用了与 Java 编程语言相似的循环和条件结构,并且遵循类似的语法模式来表示这些结构。
虽然 Rhino 与本系列文章中介绍的其他编程语言具有很多共同点,但对于 Java 平台上的编程而言,它也有独特之处(可能有时有些不同的地方)。Rhino 是一种 基于原型的(prototype-based)语言,而不是一种 基于类的(class-based)语言。在 Rhino 中,您可以构建对象,而不是类。除了类的对象实例之外,这样做还可以让您避免构建和操作类的开发、部署和执行成本。正如您将在文中发现的那样,诸如 Rhino 这类基于原型的语言在开发和运行基于 GUI 的应用程序时尤其有效。
可以从 Mozilla 的 Web 站点(请参阅 参考资料) 上下载最新的 Rhino 引擎(在撰写本文时是 Rhino-1.5r5)。将软件包解压至选定的目录中。顶层目录 rhino1_5r5 是软件包的一部分。该目录中包含一些文档、例子、源代码和 js.jar 文件,而 js.jar 文件应该包含在 classpath 中。
我将广泛地使用交互式 shell,它是在 org.mozilla.javascript.tools.shell包中实现的。可以调用这个 shell,以交互模式(在这种模式中,可以输入要计算的表达式或要执行的代码)或批处理模式运行它。在批处理模式中, -e 选项可以用来运行包含 JavaScript/Rhino 源代码的字符串, -f 选项可以用来执行包含脚本代码的文件。例如,输入下面的命令将以交互模式调用解释器:
![]() |
|
javaorg.mozilla.javascript.tools.shell.Main |
然后您应该会看到解释器的版本号,后面跟着提示符 js> 。按 Ctrl+Z (Windows 系统上)或 Ctrl+D(Unix 系统上),就可以退出这个 shell。
![]() ![]() |
![]()
|
在 开始学习 Rhino 基础知识之前,先了解一下有关 JavaScript 的起源和目的可能会非常有用,JavaScript 为 Rhino 提供了很多独特的特性。JavaScript 的历史与 Web 浏览器动态表示和操作 Web 页面内容的能力是相符合的。JavaScript 的第一个版本(最初称为 LiveScript)是由 Netscape Communications 公司于 1995 年发布的,它是 Netscape Navigator 2.0 Web 浏览器的一部分。JavaScript 打算为程序员提供一种简单而直观的方法,编写一些可以在 Web 页面上下文中执行任务的简单脚本。在随后的一年中,Microsoft 引入了 JScript,它自己的用于 Internet Explorer 的 JavaScript 端口。
这两个版本的 JavaScript 都包括一个基于对象的 API,称为 文档对象模型(Document Object Model)或 DOM,用以访问和操作 Web 页面的内容。JavaScript 的第三个实现是一种新的脚本语言,称为 EcmaScript,其目的是对自己和 DOM 进行标准化。不幸的是,Microsoft 和 Netscape 都没有完全实现 EcmaScript 标准,因此到今天都还存在兼容方面的问题。
随着 Java 语言在 20 世纪 90 年代末期取得的成功,Netscape 计划发布 Javagator,它是 Navigator 中一个 100% 的纯 Java 实现。虽然 Javagator 从来没能开花结果,但是 Netscape 对 JavaScript 的移植(称为 Rhino)已经经过时间的考验存活了下来。Rhino 是 JavaScript 1.5 脚本语言的一个 100% 的纯 Java 实现,不包含 DOM API。实际上,Rhino 有时仍然被当作 Netscape 的基于 Java 的 JavaScript。
![]() ![]() |
![]()
|
在 Rhino 的第一个发行版本中,Netscape 显然想利用 Java 编程语言所取得的成功。在该公司自己的脚本语言中,很明显地从 Java 语言中借用了一些基本的语法。这使得它特别适合 Java 开发人员学习和使用。例如,考虑一下清单 1 中给出的 Java 函数与 Rhino 函数之间的相似性,Rhino 函数测试了给定的数字是否为素数:
清单 1. Rhino 函数:这个数字是素数吗?
function isPrime (num) |
除了几处例外,该程序与 Java 程序非常类似:
- 圆括号用来分隔代码块。
-
for 和
if 结构的语法与 Java 语言的相同。
- Rhino 与 Java 语言采用相同的算术和条件操作符(例如,对
sqrRoot变量的赋值),甚至可以支持类似的访问其他算术函数的方法。
- Rhino 允许使用预先定义的布尔常量 true 和 false 。
虽然没有在这里显示,但您应该注意到,Rhino 的 while 和 do...while 循环的结构都与 Java 语言的相同。
![]() ![]() |
![]()
|
当然,Rhino 和 Java 语言之间有一些显著的区别。首先,由于 Rhino 是采用动态类型的语言,因此在函数和变量的声明中看不到类型。您可以使用 function 关键字开始函数的声明,使用 var 关键字来声明局部变量(这与全局变量不同),但是不用包括正在声明的变量的类型。Rhino 运行库将在执行过程中推断变量的类型。与 Java 语言不同,Rhino 没有语句结束符(在 Java 语言中是分号),不过它也可以支持语句结束符,这是可选的。
Rhino 和 Java 语言另外一个主要区别是,您可以从解释器中运行如清单 1 所示的程序。如果假设函数的定义在某一个文件中,例如 isprime.js,那么就可以在解释器的提示符中输入下面的命令,其中 path 指向存储该文件的绝对路径:
load(" |
在返回提示符之前,需要在 Rhino 的解释器中输入下列命令,然后立即就会看到结果 true :
isPrime(37) |
采用这种方法,Rhino 解释器的功能类似于“便签本”,可以在其中输入简单的 Java 代码块进行测试和调试。由于这两种语言的语法是如此相似,因此通常可以从 Java 程序中剪切一些相关代码,将其粘贴到 .js 文件中,并在解释器中加载该文件,然后从 shell 中调用它。
当然,并不是 Rhino 语言与 Java 语言的所有区别都如此微小。在下一节中,您将看到 Rhino 用来区分自己的一些最有用的方法,我们将从大家比较熟悉的文本表示和经常使用的数据类型入手,其中包括数组、hash 表(或称为 联合数组(associative arrays))、正则表达式、函数以及您可能碰到的对象等。
![]() ![]() |
![]()
|
Rhino 中的数组可以表示为放在方括号中的一串使用逗号分隔的数值列表。因此,下面就是 Rhino 中的数组:
|
第三个例子说明数组中的元素不一定非得是简单类型。
联合数组(associative array)是另外一种数据类型,它可以表示为一个字符串。联合数组在其他语言中有时称为 词典(dictionary)或 hash 表,它是一系列关键字-值对,关键字和值使用冒号(:)分隔开。Rhino 中的联合数组的作用与 Java 语言中的 java.util.HashMap 非常相似。下面是 Rhino 中的联合数组:
|
可以使用两种方法来引用该列表中的元素。要设置刚才定义的 person hash 表中的 age 属性,您可以这样使用: person["age"] = 39 ,或者使用 person.age = 39 。要读取该值并将其保存在变量 myAge 中,您可以这样使用: myAge = person["age"] ,或者使用 myAge = person.age .
Rhino 将典型的有索引的数组作为一种特殊的联合数组来对待:它们只是一些关键字是正整数的联合数组。因此,下面这两行代码实际上定义的是完全相同的数组:
a1 = ["fee", "fi", "fo"] |
Rhino 提供了一种特殊的循环结构: for...in 结构,它可以通过联合数组中的属性进行循环。下列代码输出了刚才定义的 person 联合数组,以及这些属性的值:
for (prop in person) { |
当然,我是通过一个类 hash 表结构实现循环的,因此不能保证每个属性及其值都可以输出。
![]() ![]() |
![]()
|
与数组类似,在 Rhino 中也可以使用 正则表达式来表示文本,使用 Perl 和其他脚本语言的用户应该非常熟悉其语法。在 Rhino 中,当表示为文字值时,正则表达式是通过正向斜线(/)来分隔的。
在 Rhino 中,正则表达式被传递给字符串对象的方法,以便更简单地执行文本处理任务。例如,下面的第一行代码定义了一个正则表达式,它可以匹配算术表达式中的正整数和标准算术操作符。第二行代码通过调用 match 函数处理给定的表达式,如下所示:
tokenExpr = /d+|[+-*/]/g |
结果是生成一个 Rhino 字符串数组,数组的元素包含以下内容:
"38", "-", "4", "+","98", "/", and "5" |
![]() ![]() |
![]()
|
最后,Rhino 提供了 函数数据类型。正如前面介绍的那样,Rhino 作为第一类数据类型支持函数 —— 可以从函数中返回,也可以传递到函数中,还可以在变量声明中使用。因此,我可以在解释器提示符中编写下面的代码,并得到结果 9,这是定义平方函数的期望结果:
square = function (x) { return x * x } |
按照这个平方函数的定义,我输入了以下内容:
square(3) |
这样定义的函数不但可以用来处理文本,还可以用来处理其他数据。例如,我可以定义一个如下所示的联合数组函数:
fnList = { |
然后我可以对数组中的列表循环调用该函数,输出每个函数的值,就像它是一个数字一样。例如,我可以编写下面的代码:
for (fnName in fnList) { |
将获得如下所示结果:
The square of 3 is 9 |
![]() ![]() |
![]()
|
在 使用联合数组并且将函数表示为文本之后,就可以在 Rhino 中将任何对象表示为联合数组。实际上,对象的文本表示只不过是一个联合数组,它可能包含某些函数作为一些值。下面这个例子将展示在 Rhino 中使用对象是多么简单,Rhino 处理这些对象表现得多么强大。在开始这个例子之前,请再次考虑 person hash 表的例子:
person = {name:"Mike Squillace", age:37, position:"software engineer"} |
在 Rhino 中,这是一个联合数组的文本表示,不过更确切的说,它是一个对象的文本表示。这种表示在 Rhino 中也称为 对象初始化。刚才定义的值的类型是由解释器在输入上面的定义之后根据下面的代码进行判断的:
[object Object] |
上面的代码说明了变量 person 中存放的值是 Object类型的。
然后,我将向您展示,在重新定义 person 对象,以包含检索该对象的第一个名称的函数时,会出现什么样的情况。我将通过编写下面的代码来展示这一点:
person = { |
该函数(更确切地说是方法) getFirstName 使用了 this 指针来引用当前的对象,并对 name 属性调用 split 方法。然后, split 方法返回一个数组,其中保存了根据空格字符将给定字符串分割成子字符串的结果。最后取得该数组的第一个值并返回。
我们已经很熟悉调用新的 getFirstName 函数的方式,如下所示:
person.getFirstName() |
圆括号告诉 Rhino 解释器我正在调用一个函数,而不是简单地引用一个对象的属性。然而要注意的是,该函数本身也只是另一个 person 对象的属性,如果没有圆括号,它将引用一个未定义的值。
Rhino 还可以允许动态地为对象添加属性和方法。例如,如果想添加一个检索通过 person 对象表示的某人名字的方法,那么只需简单输入下列代码即可:
person.getLastName = function () {return this.name.split(" ")[1]} |
现在,当我输入下面的方法时,就可以得到想要的结果,在本例中,这个结果是 "Squillace":
person.getLastName() |
(注意,您可以使用 delete 操作符来删除任何属性,例如 delete person.getLastName 。)
![]() ![]() |
![]()
|
虽然上面这个例子非常有趣,但是您可能不希望一直使用对象初始化来定义个别某些人。幸运的是,Rhino 提供了另外一种创建对象的方法:使用 构造函数(constructor function)。例如,下面的函数可以作为一名 Person 对象的构造函数:
function Person (name, age, job) { |
拥有构造函数之后,就可以使用 new 操作符来创建对象了,如下所示:
mike = new Person("Mike Squillace", 37, "software engineer") |
任何函数都可以用作构造函数,不过通常希望使用定义(使用 this 指针来引用正在定义的对象)对象属性的函数以及为这些属性赋值(或函数)的函数作为构造函数。
作为一个 Java 开发人员,您很可能会认为下一个步骤是在 Rhino 中定义 Person 类。实际上,Rhino 并不需要这样定义类 —— 因为它根本就不使用类!在 Rhino 中既没有类,也没有类的实例,只有特定的对象。当调用 new 操作符时,构造函数就为对象创建一个所谓的 原型(prototype);也就是说,它创建了一个 模板(template),从中构建给定类型的对象。
![]() |
|
在诸如 Rhino 之类的基于原型的语言中,您可以修改特定对象的属性,或者其原型的属性。例如,如果想为刚才定义的 mike 对象中添加一个特殊属性,可以使用下面的方法:
mike.disability = "blind" |
我还可以通过引用 Person 构造函数的原型属性来修改 Person 的属性。如果以后想为所有从这个构造函数中派生出来的对象都添加一个 birthdate 属性,可以使用下面的方法:
Person.prototype.birthdate = null |
然后使用:
mike.birthDate = new Date(66, 10, 3) |
还要注意基于 Person 原型创建的新对象都有 birthdate 属性,因此下面的代码是有效的:
jami = new Person("Jami Bomer", 25, "unemployed") |
![]() ![]() |
![]()
|
最后我们将给出一个真正的例子来结束对 Rhino 的介绍。由于基于原型的语言在构建 GUI 应用程序时特别有效,所以我将介绍使用 Rhino 构建 GUI 应用程序的步骤。
这 个例子设计用来展示到目前为止介绍的 Rhino 的很多特性和概念,并向您展示一些新的内容。因此,我将回顾基于 GUI 的应用程序,用它来输入一家大型的软件公司的员工个人记录。这个 GUI 本身是管理大型公司员工记录系统的前端。当然,该例还远远不够,但是它可以强调 Rhino 脚本语言中的一些最重要的概念。
该 GUI 以 Java Swing GUI 框架为基础,它包括两个基本的面板。上面的面板中有一个 javax.swing.JTabbedPane ,其中包括用来输入雇员数据的任意数量的面板。您可能只对其中的两个面板感兴趣:一个让您输入员工的基本信息,另外一个让您记录每个员工出版的著作。基本信息面板如图 1 所示:
图 1. 基本信息面板

图 2 显示了员工出版的著作的面板
图 2. 出版的著作的面板

主 GUI 的下面那个面板中包括一些用来添加或修改员工记录、清空面板中的字段以及退出程序的按钮。前两个按钮会因为当前正在编辑的信息的面板的不同而有所不同。例如, Clear按钮应该清空当前处于活动状态的标签面板中的字段,而 Add按钮应该只修改与该面板有关的数据。( Exit按钮总是不保存数据就退出程序。)
为了简便起见,后端只包含两个实体。首先,我将构建并修改一个两个数组,其关键字(或 indecies)是员工的社会安全号。与每个社会安全号对应的是在每个面板中输入的员工数据。我还使用了一个 Employee 原型,来保存在第一个面板中输入的员工的基本信息。所有其他记录都只有在需要时才会被创建并添加到正在编辑的员工实例中。
清单 2 中提供了用于 MainFrame 构造函数、实例和显示画面所使用的代码:
清单 2. Employee GUI 主框架的构造函数
importPackage(java.awt, java.awt.event) |
清单 2 从三个 importXXX 语句开始。存在两个版本的 importPackage 语句。我使用 importPackage 语句导入 java 包中的其他包,然后使用逗号分隔要导入的包名列表。对于所有其他包(例如 javax.swing ),必须使用 Packages 对象来引用想导入的包,在 Packages.javax.swing 中就是这样。最后,我使用 importClass 语句导入单个类。
正如前面所介绍的,在诸如 Rhino 之类的基于原型的语言中,可以只创建单个对象,而不是类。在这里,我将创建一个 Rhino MainFrame 对象,它将作为 GUI 的主框架。为了方便起见,我在开始时就创建了两个局部变量来保存该框架的内容面板以及 JTabbedPane 。
接下来,我创建了 Rhino GeneralInfoPanel 对象,并将其分配给 this.gip ,从而将其作为 MainFrame 对象的一个成员。 GeneralInfoPanel 会被用来输入基本信息,例如名字、社会安全号、部门以及员工的等级等。最后,我添加了 infoPanel (实际的 javax.swing.JPanel )作为 GeneralInfoPanel 对象的一个成员。 infoPanel 被添加到表中,名字是由新实例化的 GeneralInfoPanel ID 成员提供的。
注意,在这些代码中, GeneralInfoPanel 不是一个子类,也没有从 javax.swing.JPanel 中继承任何内容。它是其成员之一,是一个 JPanel ,可以通过 infoPanel 成员对其进行访问。(很快您就会更好地理解这种方法的原理。)
GUI 的 PublicationsPanel 对象的处理方式与 GeneralInfoPanel 的类似。一旦完成这个过程,我们就可以创建组件映射 compMap。
组件映射(component map)将 Rhino Panel 对象的 ID 作为关键字。由于 GUI 的按钮必须根据表中的活动面板进行不同的操作,因此我希望不但可以引用这些面板,而且可以引用它们的成员。具体地说,我希望引用在单击 Add或 Clear 按钮时需要执行的函数。(按照 Java 开发人员更熟悉的术语来说,您需要使用 Panel 对象来实现一个接口,其中包括执行所需要的 clear he add 函数的方法。)
由于表项中的组件名和组件映射中的关键字都是通过 Panel 对象中的 id属性定义的,因此我可以很容易使用下面的调用访问活动表项:
compMap[tabbedPane.selectedComponent.name].doAdd() |
最后,我创建了 buttonPanel 和按钮,并添加了一些 JFrame 的内容面板。
Rhino 的特性之一是可以将 java.awt.event.ActionListener 的实现附加到每个按钮上。例如,考虑 addButton 的 ActionListener :
addButton.addActionListener( |
org.mozilla.javascript 包中的 JavaAdapter 类可以使得向 Rhino 中的组件添加监听程序变得特别简单。 JavaAdapter 是从与对象一起实现的接口中创建的,该对象包含了一个与将为该接口实现的方法名匹配的关键字。该值是在事件发生时要调用的函数。在这里,该函数只是简单地将 addButton 的工作传递给表项中活动面板的 doAdd 方法。
还要注意的是,我还没有一个用于这个 GUI 的 JFrame 。实际上,我从来都没有一个 JFrame ,而只有一个 MainFrame Rhino 对象。刚才构建的 MainFrame 对象的行为与 JFrame 类似,因为这条语句后面是构造函数:
MainFrame.prototype = new JFrame |
该语句将导致与 MainFrame 对象共享新实例化的 JFrame 对象的属性和方法。您将与 JFrame 对象共享属性与方法 —— 我并没有继承 JFrame 类。(当然,我创建的任何 MainFrame 对象都会与这个 JFrame 实例共享属性和方法。)
我正在处理一个 Rhino (而不是 Java) 对象,这意味着我无法在构建 Rhino XXXPanel 对象时使用类似的技术。如果要创建这些对象,然后在语句下面使用它们的构造函数,就不能包含 JPanel ,因此也就不能将其传递给 MainFrame 中 contentPane 对象的 add 方法,或者将它传递给 JTabbedPane 的 addTab 方法:
XXXPanel.prototype = new JPanel |
这是 Rhino 中另外一个要注意的地方,您使用的是对象,对象之间的关系是共享属性与方法,而不是一个类层次中的成员关系。
GeneralInfoPanel 和 Employee 原型
MainFrame 表项中的第一个面板是 GeneralInfoPanel ,它包括一些输入基本员工信息的字段。对于大部分内容来说,这段代码与 Java 非常类似,如清单 3 所示:
清单 3. GeneralInfoPanel 构造函数片断
function GeneralInfoPanel () |
上面的代码与 Java 代码的主要区别在于,我没有使用继承方法来构建 GeneralInfoPanel ,而是将 JPanel 作为对象的一个实例成员,其原因应该非常清楚,我们已经讨论过了。
如果正在编写 Java 代码,那么下一个步骤将是扩展 JPanel ,并在一个类中实现所有适当的监听程序接口。在 Rhino 中,可以持有将添加到 JPanel 中的组件,并为该对象的数据成员分配函数来处理事件,通过以上操作可以映射该行为。例如, MainFrame 的 addButton 总是调用该面板对象的 doAdd 方法。清单 4 给出了 GeneralInfoPanel 使用的 doAdd 方法:
清单 4. GeneralInfoPanel 的 doAdd 方法的代码
this.doAdd = function () { |
下一个步骤是通过检索所需要的员工来测试结果。如果找到记录,我将使用 add 操作对记录进行简单的修改。如果没有找到记录,就使用面板中适当的字段,调用 Employee 构造函数创建一个新的 Employee 原型。下面是 Employee 的构造函数:
// employee prototype |
您可能已经注意到 Employee 原型并没有包括雇员出版工作的字段。尽管我希望记录该数据,但是重要的一点是要考虑到并不是所有的员工都出版了著作。如果使用 Java 语言编写该程序,那么 Employee 就会需要使用 publications 数据成员来反映编辑的信息;否则,就不会添加该字段。然而在 Rhino 中,您可以在认为合适时添加或修改 Employee 的原型,如清单 5 所示:
清单 5. PublicationsPanel 的 doAdd 方法
this.doAdd = function () { |
同样,构建对象而不是构建类的优点是可以避免存储其他数据的负载。
PublicationsPanel 和 GeneralInfoPanel 之间的主要区别(除了所需字段中的明显不同之处外)是 doAdd 方法,如清单 5 所示。我首先将查看该员工是否有一个出版列表。(访问不存在的字段时,Rhino 总是返回值 null 作为结果。)如果没有,就创建一个空列表,在其中保存出版记录。如果有,就将该记录写入现有的列表中。注意,只需创建该字段一次即可,而且只在单击 PublicationsPanel 的 Add按钮时才需要创建它。当然,我可以添加其他面板来添加或修改其他员工的记录。通过这种方法,Rhino 可以让您逐步构建员工对象,根据需要添加和修改这些对象,而不用担心它们与之前的定义表示和静态表示是否一致。
![]() ![]() |
![]()
|
Rhino 是一种轻量级的、功能强大的脚本语言。其语法与 Java 编程语言类似,都广泛采用了 Java 语言中所没有的数据类型,这使它成为在 Java 平台上快速进行开发的一种优秀工具。正如本文介绍的那样,Rhino 使用原型而不是类,这使它比很多脚本语言更适合开发 GUI 应用程序,在考虑性能和风格等因素时更是如此。
Rhino 的主要缺点也正是它的强大之处:它的基础是 Java 语言,这使得它简单易学,但是有时对于一种脚本语言来说,它又太过复杂。Rhino 的基于原型的方法是把双刃剑;其良好的品质使得它成为适合某些任务的惟一选择,在其他情况中却又似乎太过单调乏味。对于那些从 Java 环境中转换过来的开发人员来说更是如此。
这就是说,Rhino 可能是本系列文章中介绍的所有脚本语言中最流行的一种,因此吸引了众多求知欲旺盛而又见多识广的开发人员的注意力。在处理 Web 浏览器中的内容来动态描绘和操作 Web 内容的时候,Rhino 无疑是首选。