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 的应用程序时尤其有效。
开始使用 Rhino
可以从 Mozilla 的 Web 站点(请参阅 参考资料)上下载最新的 Rhino 引擎(在撰写本文时是 Rhino-1.5r5)。将软件包解压至选定的目录中。顶层目录 rhino1_5r5 是软件包的一部分。该目录中包含一些文档、例子、源代码和 js.jar 文件,而 js.jar 文件应该包含在 classpath 中。
我将广泛地使用交互式 shell,它是在 org.mozilla.javascript.tools.shell
包中实现的。可以调用这个 shell,以交互模式(在这种模式中,可以输入要计算的表达式或要执行的代码)或批处理模式运行它。在批处理模式中, -e
选项可以用来运行包含 JavaScript/Rhino 源代码的字符串, -f
选项可以用来执行包含脚本代码的文件。例如,输入下面的命令将以交互模式调用解释器:
 |
关于本系列文章 虽然 alt.lang.jre 系列文章的大部分读者都对 Java 语言以及它如何在跨平台的虚拟机上运行都非常熟悉,但是却很少有人知道,Java 运行时环境可以支持除 Java 语言之外的其他语言。本系列文章是对 JRE 可用语言的一个概括性介绍。这里介绍的大部分语言都是开源的,可以免费使用,不过也可以购买商业产品。JRE 可以支持本系列文章中介绍的所有语言,这些文章的作者都相信所介绍的语言增强了 Java 平台的动态特性和灵活性。 |
|
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。
JavaScript 中的 "Java"
在 Rhino 的第一个发行版本中,Netscape 显然想利用 Java 编程语言所取得的成功。在该公司自己的脚本语言中,很明显地从 Java 语言中借用了一些基本的语法。这使得它特别适合 Java 开发人员学习和使用。例如,考虑一下清单 1 中给出的 Java 函数与 Rhino 函数之间的相似性,Rhino 函数测试了给定的数字是否为素数:
清单 1. Rhino 函数:这个数字是素数吗?
function isPrime (num)
{
if (num <= 1) {
print("Please enter a positive integer >= 2.")
return false
}
var prime = true
var sqrRoot = Math.round(Math.sqrt(num))
for (var n = 2; prime & n <= sqrRoot; ++n) {
prime = (num % n != 0)
}
return prime
}
|
除了几处例外,该程序与 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("<path>/isprime.js")
|
在返回提示符之前,需要在 Rhino 的解释器中输入下列命令,然后立即就会看到结果 true
:
采用这种方法,Rhino 解释器的功能类似于“便签本”,可以在其中输入简单的 Java 代码块进行测试和调试。由于这两种语言的语法是如此相似,因此通常可以从 Java 程序中剪切一些相关代码,将其粘贴到 .js 文件中,并在解释器中加载该文件,然后从 shell 中调用它。
当然,并不是 Rhino 语言与 Java 语言的所有区别都如此微小。在下一节中,您将看到 Rhino 用来区分自己的一些最有用的方法,我们将从大家比较熟悉的文本表示和经常使用的数据类型入手,其中包括数组、hash 表(或称为 联合数组(associative arrays))、正则表达式、函数以及您可能碰到的对象等。
数组
Rhino 中的数组可以表示为放在方括号中的一串使用逗号分隔的数值列表。因此,下面就是 Rhino 中的数组:
numbers = [0, 1, 2, 3, 5, 24]
names = ["Mike", "Joe", "Betty", "Therese", "Lynn Marie"]
arrayOfArrays = [1, [2, 3], [3, 4, 5, [6, 7]], 8]
|
第三个例子说明数组中的元素不一定非得是简单类型。
联合数组
联合数组(associative array)是另外一种数据类型,它可以表示为一个字符串。联合数组在其他语言中有时称为 词典(dictionary)或 hash 表,它是一系列关键字-值对,关键字和值使用冒号(:)分隔开。Rhino 中的联合数组的作用与 Java 语言中的 java.util.HashMap
非常相似。下面是 Rhino 中的联合数组:
person = {name:"Mike Squillace", age:37, position:"software engineer"}
link = {text:"IBM Home", url:"http://www.ibm.com"}
|
可以使用两种方法来引用该列表中的元素。要设置刚才定义的 person
hash 表中的 age
属性,您可以这样使用: person["age"] = 39
,或者使用 person.age = 39
。要读取该值并将其保存在变量 myAge 中,您可以这样使用: myAge = person["age"]
,或者使用 myAge = person.age
.
Rhino 将典型的有索引的数组作为一种特殊的联合数组来对待:它们只是一些关键字是正整数的联合数组。因此,下面这两行代码实际上定义的是完全相同的数组:
a1 = ["fee", "fi", "fo"]
a2 = {0:"fee", 1:"fi", 2:"fo"}
|
通过属性进行循环
Rhino 提供了一种特殊的循环结构: for...in
结构,它可以通过联合数组中的属性进行循环。下列代码输出了刚才定义的 person
联合数组,以及这些属性的值:
for (prop in person) {
print("person[" + prop + "] = " + person[prop])
}
|
当然,我是通过一个类 hash 表结构实现循环的,因此不能保证每个属性及其值都可以输出。
正则表达式
与数组类似,在 Rhino 中也可以使用 正则表达式来表示文本,使用 Perl 和其他脚本语言的用户应该非常熟悉其语法。在 Rhino 中,当表示为文字值时,正则表达式是通过正向斜线(/)来分隔的。
在 Rhino 中,正则表达式被传递给字符串对象的方法,以便更简单地执行文本处理任务。例如,下面的第一行代码定义了一个正则表达式,它可以匹配算术表达式中的正整数和标准算术操作符。第二行代码通过调用 match
函数处理给定的表达式,如下所示:
tokenExpr = //d+|[/+/-/*//]/g
"38-4+98/5".match(tokenExpr)
|
结果是生成一个 Rhino 字符串数组,数组的元素包含以下内容:
"38", "-", "4", "+","98", "/", and "5"
|
函数显式声明
最后,Rhino 提供了 函数数据类型。正如前面介绍的那样,Rhino 作为第一类数据类型支持函数 —— 可以从函数中返回,也可以传递到函数中,还可以在变量声明中使用。因此,我可以在解释器提示符中编写下面的代码,并得到结果 9,这是定义平方函数的期望结果:
square = function (x) { return x * x }
|
按照这个平方函数的定义,我输入了以下内容:
这样定义的函数不但可以用来处理文本,还可以用来处理其他数据。例如,我可以定义一个如下所示的联合数组函数:
fnList = {
square:function (x) {return x * x},
cube:function (x) {return x * x * x},
sqrt:function (x) {return Math.sqrt(x)}
}
|
然后我可以对数组中的列表循环调用该函数,输出每个函数的值,就像它是一个数字一样。例如,我可以编写下面的代码:
for (fnName in fnList) {
print("The " + fnName + " of " + 3 + " is " + fnList[fnName](3))
}
|
将获得如下所示结果:
The square of 3 is 9
the sqrt of 3 is 1.7320508075688772
The cube of 3 is 27
|
Rhino 中的对象
在使用联合数组并且将函数表示为文本之后,就可以在 Rhino 中将任何对象表示为联合数组。实际上,对象的文本表示只不过是一个联合数组,它可能包含某些函数作为一些值。下面这个例子将展示在 Rhino 中使用对象是多么简单,Rhino 处理这些对象表现得多么强大。在开始这个例子之前,请再次考虑 person
hash 表的例子:
person = {name:"Mike Squillace", age:37, position:"software engineer"}
|
在 Rhino 中,这是一个联合数组的文本表示,不过更确切的说,它是一个对象的文本表示。这种表示在 Rhino 中也称为 对象初始化。刚才定义的值的类型是由解释器在输入上面的定义之后根据下面的代码进行判断的:
上面的代码说明了变量 person
中存放的值是 Object类型的。
添加方法
然后,我将向您展示,在重新定义 person
对象,以包含检索该对象的第一个名称的函数时,会出现什么样的情况。我将通过编写下面的代码来展示这一点:
person = {
name:"Mike Squillace",
age:37,
position:"software engineer",
getFirstName:function () {return this.name.split(" ")[0]}
}
|
该函数(更确切地说是方法) getFirstName
使用了 this
指针来引用当前的对象,并对 name 属性调用 split
方法。然后, split
方法返回一个数组,其中保存了根据空格字符将给定字符串分割成子字符串的结果。最后取得该数组的第一个值并返回。
我们已经很熟悉调用新的 getFirstName
函数的方式,如下所示:
圆括号告诉 Rhino 解释器我正在调用一个函数,而不是简单地引用一个对象的属性。然而要注意的是,该函数本身也只是另一个 person
对象的属性,如果没有圆括号,它将引用一个未定义的值。
添加更多的方法
Rhino 还可以允许动态地为对象添加属性和方法。例如,如果想添加一个检索通过 person
对象表示的某人名字的方法,那么只需简单输入下列代码即可:
person.getLastName = function () {return this.name.split(" ")[1]}
|
现在,当我输入下面的方法时,就可以得到想要的结果,在本例中,这个结果是 "Squillace":
(注意,您可以使用 delete
操作符来删除任何属性,例如 delete person.getLastName
。)
使用原型
虽然上面这个例子非常有趣,但是您可能不希望一直使用对象初始化来定义个别某些人。幸运的是,Rhino 提供了另外一种创建对象的方法:使用 构造函数(constructor function)。例如,下面的函数可以作为一名 Person
对象的构造函数:
function Person (name, age, job) {
this.name = name || "<unknown>"
this.age = age
this.job = job || "<unemployed>"
this.getFirstName = function () {return this.name.split(" ")[0]}
this.getLastName = function () {return this.name.split(" ")[1]}
}
|
拥有构造函数之后,就可以使用 new
操作符来创建对象了,如下所示:
mike = new Person("Mike Squillace", 37, "software engineer")
|
任何函数都可以用作构造函数,不过通常希望使用定义(使用 this
指针来引用正在定义的对象)对象属性的函数以及为这些属性赋值(或函数)的函数作为构造函数。
无类编码
作为一个 Java 开发人员,您很可能会认为下一个步骤是在 Rhino 中定义 Person
类。实际上,Rhino 并不需要这样定义类 —— 因为它根本就不使用类!在 Rhino 中既没有类,也没有类的实例,只有特定的对象。当调用 new
操作符时,构造函数就为对象创建一个所谓的 原型(prototype);也就是说,它创建了一个 模板(template),从中构建给定类型的对象。
 |
基于类的语言与基于原型的语言的比较 诸如 C++ 和 Java 之类的 基于类的语言使用一个类定义来表示一组具有特定属性集的对象,包括用来表示类的实例和这些实例可以执行的任务类型的数据。一旦编写并编译了类定义,就不能在运行时再对其进行修改,该列的所有实例都包含该类中定义的数据和方法(也可能 只有这些数据或方法)。在这种情况下,所有的类都是 Class 类的实例。 基于原型的语言并不区分类和实例;它们只识别基于原型或模板的特殊对象。原型定义了对象初始化时使用的属性。基于原型的实例的属性以及原型本身在任何时间都可以修改。因此,不需要一个“类”对象或数据类型。从某种意义上来说,我们可以认为基于原型的语言是基于对象的差异性,而不是基于对象的共同性。 |
|
在诸如 Rhino 之类的基于原型的语言中,您可以修改特定对象的属性,或者其原型的属性。例如,如果想为刚才定义的 mike
对象中添加一个特殊属性,可以使用下面的方法:
mike.disability = "blind"
|
我还可以通过引用 Person
构造函数的原型属性来修改 Person
的属性。如果以后想为所有从这个构造函数中派生出来的对象都添加一个 birthdate
属性,可以使用下面的方法:
Person.prototype.birthdate = null
|
然后使用:
mike.birthDate = new Date(66, 10, 3)
// months are zero-based: this is 11/3/66
|
还要注意基于 Person
原型创建的新对象都有 birthdate
属性,因此下面的代码是有效的:
jami = new Person("Jami Bomer", 25, "unemployed")
jami.birthdate = new Date(79, 5, 28)
|