Java-脚本编程教程-集成-Groovy-和-JS-全-

Java 脚本编程教程:集成 Groovy 和 JS(全)

协议:CC BY-NC-SA 4.0

一、入门指南

在本章中,您将学习:

  • 什么是 Java 脚本
  • 如何从 Java 执行您的第一个脚本
  • 如何使用来自 Java 的 JRuby、Jython 等其他脚本语言
  • javax.script 原料药
  • 如何发现和实例化脚本引擎

Java 中的脚本是什么?

有人认为 Java 虚拟机(JVM)可以执行只用 Java 编程语言编写的程序。然而,事实并非如此。JVM 执行语言无关的字节码。如果程序可以被编译成 Java 字节码,它可以执行用任何编程语言编写的程序。

脚本语言是一种编程语言,它使您能够编写由运行时环境评估(或解释)的脚本,运行时环境称为脚本引擎(或解释器)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。解释器解析脚本,产生中间代码,中间代码是程序的内部表示,并执行中间代码。解释器将脚本中使用的变量存储在称为符号表的数据结构中。

通常,与编译的编程语言不同,脚本语言中的源代码(称为脚本)不被编译,而是在运行时被解释。然而,用一些脚本语言编写的脚本可以被编译成 Java 字节码,由 JVM 运行。

Java 6 向 Java 平台添加了脚本支持,允许 Java 应用执行用 Rhino JavaScript、Groovy、Jython、JRuby、Nashorn JavaScript 等脚本语言编写的脚本。支持双向通信。它还允许脚本访问由宿主应用创建的 Java 对象。Java 运行时和脚本语言运行时可以相互通信并利用彼此的特性。

Java 对脚本语言的支持来自 Java 脚本 API。Java 脚本 API 中的所有类和接口都在javax.script包中。

在 Java 应用中使用脚本语言有几个好处:

  • 大多数脚本语言都是动态类型的,这使得编写程序更加简单。
  • 它们为开发和测试小型应用提供了一种更快捷的方式。
  • 最终用户可以进行定制。
  • 脚本语言可以提供 Java 中没有的特定领域的特性。

脚本语言也有一些缺点。例如,动态类型有利于编写更简单的代码;然而,当一个类型被错误地解释时,它就变成了一个缺点,你必须花很多时间去调试它。

Java 中的脚本支持让您可以利用两个世界的优势:它允许您使用 Java 编程语言来开发应用的静态类型、可伸缩和高性能部分,并使用适合特定领域需求的脚本语言来开发其他部分。

我将在本书中频繁使用术语脚本引擎。脚本引擎是执行用特定脚本语言编写的程序的软件组件。通常,但不一定,脚本引擎是脚本语言的解释器的实现。Java 已经实现了几种脚本语言的解释器。它们公开了编程接口,因此 Java 程序可以与它们进行交互。

JDK 7 与一个名为 Rhino JavaScript 的脚本引擎捆绑在一起。JDK 8 用一个轻量级、更快的脚本引擎 Nashorn JavaScript 取代了 Rhino JavaScript 引擎。这本书讨论的是 Nashorn JavaScript,不是 Rhino JavaScript。请访问 www.mozilla.org/rhino 了解更多关于 Rhino JavaScript 文档的详细信息。如果你想把用 Rhino JavaScript 编写的程序迁移到 Nashorn,请访问 https://wiki.openjdk.java.net/display/Nashorn/Rhino+Migration+Guide 的 Rhino 迁移指南。如果你有兴趣在 JDK 8 中使用 Rhino JavaScript,请访问页面 https://wiki.openjdk.java.net/display/Nashorn/Using+Rhino+JSR-223+engine+with+JDK8

Java 包含一个名为jrunscript的命令行 shell,可以用来以交互模式或批处理模式运行脚本。jrunscript shell 是脚本语言中立的;JDK 7 的默认语言是 Rhino JavaScript,JDK 8 的默认语言是 Nashorn。我将在第九章中详细讨论jrunscript外壳。JDK 8 包括另一个名为jjs的命令行工具,它调用 Nashorn 引擎并提供特定于 Nashorn 的命令行选项。如果你正在使用 Nashorn,你应该使用jjs命令行工具而不是jrunscript。我将在第十章的中讨论jjs命令行工具。

Java 可以执行任何为脚本引擎提供实现的脚本语言的脚本。比如 Java 可以执行 Nashorn JavaScript、Rhino JavaScript、Groovy、Jython、JRuby 等编写的脚本。本书中的例子使用了 Nashorn JavaScript 语言。

在本书中,术语“Nashorn”、“Nashorn 引擎”、“Nashorn JavaScript”、“Nashorn JavaScript 引擎”、“Nashorn 脚本语言”和“JavaScript”作为同义词使用。

可以通过两种方式调用 Nashorn 脚本引擎:

  • 通过将引擎嵌入到 JVM 中
  • 通过使用jjs命令行工具

在这一章中,我将讨论使用 Nashorn 脚本引擎的两种方法。

执行您的第一个脚本

在本节中,您将使用 Nashorn 在标准输出上打印一条消息。您将从 Java 代码中访问 Nashorn 引擎。使用任何其他脚本语言都可以使用相同的步骤来打印消息,只有一点不同:您需要使用特定于脚本语言的代码来打印消息。在 Java 中运行脚本需要执行三个步骤:

  • 创建脚本引擎管理器。
  • 从脚本引擎管理器获取脚本引擎的实例。
  • 调用脚本引擎的eval()方法执行脚本。

脚本引擎管理器是ScriptEngineManager类的一个实例。您可以创建一个脚本引擎,如下所示:

// Create a script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

接口的一个实例代表了 Java 程序中的一个脚本引擎。一个ScriptEngineManagergetEngineByName(String engineShortName)方法用于获取一个脚本引擎的实例。要获得 Nashorn 引擎的实例,使用JavaScript作为引擎的简称,如下所示:

// Get the reference of a Nashorn engine

ScriptEngine engine = manager.getEngineByName("JavaScript");

Tip

脚本引擎的简称区分大小写。有时一个脚本引擎有多个简称。Nashorn 发动机有以下简称:nashornNashornjsJSJavaScriptjavascriptECMAScriptecmascript。您可以使用引擎的任何简称,通过使用ScriptEngineManager类的getEngineByName()方法来获得它的实例。

在 Nashorn 中,print()函数在标准输出上打印一条消息,字符串是用单引号或双引号括起来的一系列字符。下面的代码片段在一个String对象中存储了一个脚本,该脚本在标准输出中打印Hello Scripting!:

// Store a Nashorn script in a string

String script = "print('Hello Scripting!')";

如果要在 Nashorn 中使用双引号将字符串括起来,该语句将如下所示:

// Store a Nashorn script in a string

String script = "print(\"Hello Scripting!\")";

要执行脚本,需要将其传递给脚本引擎的eval()方法。脚本引擎在运行脚本时可能会抛出一个ScriptException。因此,当您调用ScriptEngineeval()方法时,您需要处理这个异常。以下代码片段执行存储在script变量中的脚本:

try {

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

清单 1-1 包含了在标准输出上打印消息的完整代码。

清单 1-1。使用 Nashorn 在标准输出上打印信息

// HelloScripting.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class HelloScripting {

public static void main(String[] args) {

// Create a script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

// Obtain a Nashorn script engine from the manager

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Store the script in a String

String script = "print('Hello Scripting!')";

try {

// Execute the script

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello Scripting!

使用 jjs 命令行工具

在上一节中,您看到了如何在 Java 程序中使用 Nashorn 脚本引擎。在这一节中,我将向您展示如何使用jjs命令行工具来执行相同的任务。刀具存储在JDK_HOME\binJRE_HOME\bin目录中。例如,如果您在 Windows 的C:\java8目录下安装了 JDK8,jjs工具的路径将会是C:\java8\bin\jjs.exejjs工具可用于执行文件中的 Nashorn 脚本或交互执行脚本。

以下是在 Windows 命令提示符下对jjs工具的调用。脚本被输入并执行。您可以使用 q uit()exit()功能退出jjs工具:

C:\>jjs

jjs> print('Hello Scripting!');

Hello Scripting!

jjs> quit()

C:\>

执行jjs命令时,可能会出现以下错误:

'jjs' is not recognized as an internal or external command, operable program or batch file.

该错误表明命令提示符无法定位jjs刀具。在这种情况下,您可以输入jjs工具的完整路径,或者在系统路径中添加包含 too 的目录。

考虑清单 1-2 中列出的代码。Nashorn 代码使用print()函数在标准输出上打印消息。代码保存在一个名为helloscripting.js的文件中。

清单 1-2:hello scripting . js 文件的内容

// helloscripting.js

// Print a message on the standard output

print('Hello Scripting!');

以下命令执行存储在helloscripting.js文件中的脚本,假设该文件存储在当前目录中:

C:\>jjs helloscripting.js

Hello Scripting!

C:\>

如果此命令显示类似以下内容的错误,则意味着该命令无法找到指定的文件,您需要指定 helloscritping.js 文件的完整路径:

java.io.FileNotFoundException: C:\helloscripting.js (The system cannot find the file specified)

命令行工具是一个很大的话题,我将用整整一章来讲述它。我会在第十章中详细讨论。

在 Nashorn 打印文本

Nashorn 为您提供了三个在标准输出上打印文本的功能:

  • print()功能
  • printf()功能
  • echo()功能

print()函数是一个 varargs 函数。您可以向它传递任意数量的参数。它将其参数转换为字符串并打印出来,用空格隔开。最后,它打印一个新行。下面两次调用print()函数是相同的:

print("Hello", "World!"); // Prints Hello World!

print("Hello World!");    // Prints Hello World!

printf()功能用于使用 printf 风格的格式化打印。这与调用 Java 方法System.out.printf()是一样的:

printf("%d + %d = %d", 10, 20, 10 + 20); // Prints 10 + 20 = 30

echo()函数与print()函数相同,只是它只在脚本模式下工作。脚本模式在第十章中讨论。

使用其他脚本语言

在 Java 程序中使用除 Nashorn 之外的脚本语言非常简单。在使用脚本引擎之前,您只需要执行一项任务:在应用类路径中包含特定脚本引擎的 JAR 文件。脚本引擎的实现者提供这些 JAR 文件。

Java 使用一种发现机制来列出其 JAR 文件已经包含在应用类路径中的所有脚本引擎。接口的一个实例用于创建和描述一个脚本引擎。脚本引擎的提供者为ScriptEngineFactory接口提供了一个实现。ScriptEngineManagergetEngineFactories()方法返回所有可用脚本引擎工厂的List<ScriptEngineFactory>ScriptEngineFactorygetScriptEngine()方法返回ScriptEngine的一个实例。工厂的其他几个方法返回关于引擎的元数据。

表 1-1 列出了在 Java 应用中使用脚本引擎之前,如何安装脚本引擎的详细信息。网站列表和说明在撰写本文时有效;它们可能在阅读时失效。但是,它们向您展示了脚本语言的脚本引擎是如何安装的。如果你对使用 Nashorn 感兴趣,你不需要在你的机器上安装任何东西。Nashorn 在 JDK 8 中可用。

表 1-1。

Installation Details for Installing Some Script Engines

脚本引擎 版本 网站(全球资讯网的主机站) 安装说明
绝妙的 Two point three groovy.codehaus.org 下载 Groovy 的安装文件;这是一个压缩文件。拉开拉链。在embeddable文件夹中查找名为groovy-all-2.0.0-rc-2.jar的 JAR 文件。将这个 JAR 文件添加到类路径中。
脚本语言 2.5.3 www.jython.org 下载 Jython 安装程序文件,它是一个 JAR 文件。提取jython.jar文件并将其添加到类路径中。
JRuby 1.7.13 www.jruby.org 下载 JRuby 安装文件。您可以选择下载一个 ZIP 文件。拉开拉链。在lib文件夹中,您将找到一个需要包含在类路径中的jruby.jar文件。

清单 1-3 显示了如何打印所有可用脚本引擎的细节。输出显示 Groovy、Jython 和 JRuby 的脚本引擎可用。它们之所以可用,是因为我已经将它们引擎的 JAR 文件添加到了我机器上的类路径中。当您在类路径中包含了脚本引擎的 JAR 文件,并且想知道脚本引擎的简称时,这个程序会很有帮助。运行该程序时,您可能会得到不同的输出。

清单 1-3。列出所有可用的脚本引擎

// ListingAllEngines.java

package com.jdojo.script;

import java.util.List;

import javax.script.ScriptEngineFactory;

import javax.script.ScriptEngineManager;

public class ListingAllEngines {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

// Get the list of all available engines

List<ScriptEngineFactory> list = manager.                getEngineFactories();

// Print the details of each engine

for (ScriptEngineFactory f : list) {

System.out.println("Engine Name:" +                         f.getEngineName());

System.out.println("Engine Version:" +

f.getEngineVersion());

System.out.println("Language Name:" +                         f.getLanguageName());

System.out.println("Language Version:" +                         f.getLanguageVersion());

System.out.println("Engine Short Names:" +                         f.getNames());

System.out.println("Mime Types:" +                         f.getMimeTypes());

System.out.println("----------------------------");

}

}

}

Engine Name:jython

Engine Version:2.5.3

Language Name:python

Language Version:2.5

Engine Short Names:[python, jython]

Mime Types:[text/python, application/python, text/x-python, application/x-python]

----------------------------

Engine Name:JSR 223 JRuby Engine

Engine Version:1.7.0.preview1

Language Name:ruby

Language Version:jruby 1.7.0.preview1

Engine Short Names:[ruby, jruby]

Mime Types:[application/x-ruby]

----------------------------

Engine Name:Groovy Scripting Engine

Engine Version:2.0

Language Name:Groovy

Language Version:2.0.0-rc-2

Engine Short Names:[groovy, Groovy]

Mime Types:[application/x-groovy]

----------------------------

Engine Name:Oracle Nashorn

Engine Version:1.8.0_05

Language Name:ECMAScript

Language Version:ECMA - 262 Edition 5.1

Engine Short Names:[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

Mime Types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]

----------------------------

清单 1-4 展示了如何使用 JavaScript、Groovy、Jython 和 JRuby 在标准输出中打印消息。如果脚本引擎不可用,程序会打印一条消息说明这一点。

清单 1-4。使用不同的脚本语言在标准输出上打印消息

// HelloEngines.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class HelloEngines {

public static void main(String[] args) {

// Get the script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

// Try executing scripts in Nashorn, Groovy, Jython, and JRuby

execute(manager, "JavaScript", "print('Hello JavaScript')");

execute(manager, "Groovy", "println('Hello Groovy')");

execute(manager, "jython", "print 'Hello Jython'");

execute(manager, "jruby", "puts('Hello JRuby')");

}

public static void execute(ScriptEngineManager manager,

String engineName,

String script) {

// Try getting the engine

ScriptEngine engine = manager.getEngineByName(engineName);

if (engine == null) {

System.out.println(engineName + " is not available.");

return;

}

// If we get here, it means we have the engine installed.                 // So, run the script

try {

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello JavaScript

Hello Groovy

Hello Jython

Hello JRuby

有时,您可能只是为了好玩而想使用脚本语言,并且您不知道用于在标准输出中打印消息的语法。ScriptEngineFactory类包含一个名为getOutputStatement(String toDisplay)的方法,您可以用它来查找在标准输出中打印文本的语法。以下代码片段显示了如何获取 Nashorn 的语法:

// Get the script engine factory for Nashorn

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

ScriptEngineFactory factory = engine.getFactory();

// Get the script

String script = factory.getOutputStatement("\"Hello JavaScript\"");

System.out.println("Syntax: " + script);

// Evaluate the script

engine.eval(script);

Syntax: print("Hello JavaScript")

Hello JavaScript

对于其他脚本语言,使用它们的引擎工厂来获得语法。

探索 javax.script 包

Java 中的 Java 脚本 API 由少量的类和接口组成。它们在javax.script包里。本章包含了对这个包中的类和接口的简要描述。我将在后面的章节中讨论它们的用法。图 1-1 显示了 Java 脚本 API 中的类和接口的类图。

A978-1-4842-0713-0_1_Fig1_HTML.jpg

图 1-1。

The class diagram for classes and interfaces in the Java Scripting API

脚本引擎和脚本引擎工厂接口

ScriptEngine接口是 Java 脚本 API 的主要接口,它的实例促进了以特定脚本语言编写的脚本的执行。

ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现。一辆ScriptEngineFactory执行两项任务:

  • 它创建脚本引擎的实例。
  • 它提供了关于脚本引擎的信息,例如引擎名称、版本、语言等等。

AbstractScriptEngine 类

AbstractScriptEngine类是一个抽象类。它为ScriptEngine接口提供了部分实现。除非实现脚本引擎,否则不能直接使用该类。

ScriptEngineManager 类

ScriptEngineManager类为脚本引擎提供了发现和实例化机制。它还维护一个键-值对的映射,作为存储状态的Bindings接口的一个实例,由它创建的所有脚本引擎共享。

可编译接口和 CompiledScript 类

可选地,可以通过脚本引擎来实现Compilable接口,该脚本引擎允许编译脚本以便重复执行,而无需重新编译。

CompiledScript类是一个抽象类。它是由脚本引擎的提供者扩展的。它以编译形式存储脚本,无需重新编译即可重复执行。请注意,使用ScriptEngine重复执行脚本会导致脚本每次都要重新编译,从而降低性能。

支持脚本编译不需要脚本引擎。如果支持脚本编译,它必须实现Compilable接口。

可调用的接口

可选地,Invocable接口可以由脚本引擎实现,该脚本引擎可以允许调用先前已经编译过的脚本中的过程、函数和方法。

绑定接口和简单绑定类

实现Bindings接口的类的一个实例是一个键-值对的映射,有一个限制,即一个键必须是非空的,非空的String。它扩展了java.util.Map接口。SimpleBindings类是Bindings接口的一个实现。

ScriptContext 接口和 SimpleScriptContext 类

接口的一个实例充当 Java 主机应用和脚本引擎之间的桥梁。它用于将 Java 主机应用的执行上下文传递给脚本引擎。脚本引擎可以在执行脚本时使用上下文信息。脚本引擎可以将其状态存储在实现ScriptContext接口的类的实例中,Java 主机应用可以访问该接口。

SimpleScriptContext类是ScriptContext接口的一个实现。

ScriptException 类

ScriptException类是一个异常类。如果在脚本的执行、编译或调用过程中出现错误,脚本引擎会抛出一个ScriptException。该类包含三个有用的方法,分别叫做getLineNumber()getColumnNumber()getFileName()。这些方法报告发生错误的脚本的行号、列号和文件名。ScriptException类覆盖了Throwable类的getMessage()方法,并在它返回的消息中包含行号、列号和文件名。

发现和实例化脚本引擎

您可以使用ScriptEngineFactoryScriptEngineManager创建脚本引擎。谁真正负责创建一个脚本引擎:ScriptEngineFactoryScriptEngineManager,或者两者都有?简单的回答是,ScriptEngineFactory总是负责创建脚本引擎的实例。下一个问题是“a ScriptEngineManager的作用是什么?”

A ScriptEngineManager使用服务提供者机制来定位所有可用的脚本引擎工厂。它搜索类路径和其他标准目录中的所有 JAR 文件。它寻找一个资源文件,这是一个名为javax.script.ScriptEngineFactory的文本文件,位于名为META-INF/services的目录下。资源文件由实现ScriptEngineFactory接口的类的完全限定名组成。每个类名在单独的一行中指定。该文件可能包含以#字符开头的注释。示例资源文件可能包含以下内容,其中包括两个脚本引擎工厂的类名:

#Java Kishori Script Engine Factory class

com.jdojo.script.JKScriptEngineFactory

#Another factory class

com.jdojo.script.FunScriptFactory

一个ScriptEngineManager定位并实例化所有可用的ScriptEngineFactory类。您可以使用ScriptEngineManager类的getEngineFactories()方法获得所有工厂类的实例列表。当您调用管理器的一个方法来获得一个基于某个标准的脚本引擎时,例如通过名称获得引擎的getEngineByName(String shortName)方法,管理器搜索该标准的所有工厂并返回匹配的脚本引擎引用。如果没有工厂能够提供匹配的引擎,经理返回null。请参考清单 1-3,了解关于列出所有可用工厂和描述它们可以创建的脚本引擎的更多细节。

现在你知道了ScriptEngineManager并不创建脚本引擎的实例。相反,它查询所有可用的工厂,并将工厂创建的脚本引擎的引用传递回调用者。

为了使讨论完整,让我们添加一个创建脚本引擎的方法。您可以通过三种方式创建脚本引擎的实例:

  • 直接实例化脚本引擎类。
  • 直接实例化脚本引擎工厂类,调用其getScriptEngine()方法。
  • 使用ScriptEngineManager类的getEngineByXxx()方法之一。

建议使用ScriptEngineManager类来获取脚本引擎的实例。这个方法允许由同一个管理器创建的所有引擎共享一个状态,这个状态是作为Bindings接口的一个实例存储的一组键-值对。ScriptEngineManager实例存储这个状态。

Tip

一个应用中可能有多个ScriptEngineManager类的实例。在这种情况下,每个ScriptEngineManager实例维护一个它创建的所有引擎共有的状态。也就是说,如果两个引擎是由ScriptEngineManager类的两个不同实例获得的,那么这些引擎将不会共享由它们的管理器维护的一个公共状态,除非您以编程方式实现。

摘要

脚本语言是一种编程语言,它使您能够编写由运行时环境评估(或解释)的脚本,运行时环境称为脚本引擎(或解释器)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。Java 脚本 API 允许您执行用任何脚本语言编写的脚本,这些脚本可以从 Java 应用编译成 Java 字节码。JDK 6 和 7 附带了一个名为 Rhino JavaScript engine 的脚本引擎。在 JDK 8 中,Rhino JavaScript 引擎已经被一个名为 Nashorn 的脚本引擎所取代。

Nashorn 发动机有两种用途:它可以嵌入到 JVM 中,直接从 Java 程序中调用,也可以使用jjs命令行工具从命令提示符中调用。

Nashorn 提供了三个在标准输出上打印文本的功能:print()printf()echo()print()函数接受可变数量的参数;它打印所有参数,用一个空格将它们分开,最后打印一个新行。printf()功能打印格式化文本;它的工作方式与 Java 编程语言中的 printf 样式的格式化方式相同。echo()功能的工作方式与print()功能相同,只是前者仅在脚本模式下调用 Nashorn 引擎时可用。

使用脚本引擎执行脚本,脚本引擎是ScriptEngine接口的一个实例。ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现,其工作是创建脚本引擎的实例并提供关于脚本引擎的细节。ScriptEngineManager类为脚本引擎提供了发现和实例化机制。一个ScriptManager维护一个键值对的映射,作为一个由它创建的所有脚本引擎共享的Bindings接口的实例。

二、执行脚本

在本章中,您将学习:

  • 如何使用ScriptEngineeval()方法执行脚本
  • 如何从 Java 代码向 Nashorn 引擎传递参数
  • 如何从 Nashorn 引擎向 Java 传递参数

使用 eval()方法

一个ScriptEngine可以执行一个String和一个Reader中的脚本。使用Reader,您可以执行存储在网络或文件中的脚本。ScriptEngine接口的eval()方法的以下版本之一用于执行脚本:

  • Object eval(String script)
  • Object eval(Reader reader)
  • Object eval(String script, Bindings bindings)
  • Object eval(Reader reader, Bindings bindings)
  • Object eval(String script, ScriptContext context)
  • Object eval(Reader reader, ScriptContext context)

eval()方法的第一个参数是脚本的源。第二个参数允许您将信息从宿主应用传递到脚本引擎,这些信息可以在脚本执行期间使用。

在第一章的中,您看到了如何使用第一版的eval()方法使用String对象来执行脚本。在这一章中,你将把你的脚本存储在一个文件中,并使用一个Reader对象作为脚本的源,它将使用第二个版本的eval()方法。下一节将讨论eval()方法的其他四个版本。通常,脚本文件会被赋予一个.js扩展名。

清单 2-1 显示了一个名为helloscript.js的文件的内容。它在 Nashorn 中只包含一个在标准输出中打印消息的语句。

清单 2-1。helloscript.js 文件的内容

// helloscript.js

// Print a message

print('Hello from JavaScript!');

清单 2-2 包含执行存储在helloscript.js文件中的脚本的 Java 程序,该文件应该存储在当前目录中。如果没有找到脚本文件,程序会在需要的地方打印出helloscript.js文件的完整路径。如果您在执行脚本文件时遇到问题,请尝试使用main()方法中的绝对路径,比如 Windows 上的C:\scripts\helloscript.js,假设helloscript.js文件保存在C:\scripts目录中。

清单 2-2。执行存储在文件中的脚本

// ReaderAsSource.java

package com.jdojo.script;

import java.io.IOException;

import java.io.Reader;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ReaderAsSource {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "helloscript.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full

// path of the script file and terminate the program.

if (! Files.exists(scriptPath) ) {

System.out.println(scriptPath.toAbsolutePath() +

" does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Get a Reader for the script file

Reader scriptReader = Files.                        newBufferedReader(scriptPath);

// Execute the script in the file

engine.eval(scriptReader);

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

}

}

Hello from JavaScript!

在实际应用中,您应该将所有脚本存储在允许修改脚本而无需修改和重新编译 Java 代码的文件中。在本章的大部分例子中,你不会遵循这个规则;您将把您的脚本存储在String对象中,以保持代码简短。

传递参数

Java 脚本 API 允许您将参数从主机环境(Java 应用)传递到脚本引擎,反之亦然。在本节中,您将看到宿主应用和脚本引擎之间的参数传递机制的技术细节。有几种方法可以将参数从 Java 程序传递给脚本。在这一章中,我将解释参数传递的最简单的形式。我将在第三章中讨论所有其他形式。

从 Java 代码向脚本传递参数

Java 程序可以向脚本传递参数。Java 程序也可以在脚本执行后访问脚本中声明的全局变量。让我们讨论一个简单的例子,Java 程序向脚本传递一个参数。考虑清单 2-3 中向脚本传递参数的程序。

清单 2-3。从 Java 程序向脚本传递参数

// PassingParam.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class PassingParam {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Store the script in a String. Here, msg is a variable

// that we have not declared in the script

String script = "print(msg)";

try {

// Store a parameter named msg in the engine

engine.put("msg", "Hello from Java program");

// Execute the script

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello from Java program

程序将一个脚本存储在一个String对象中,如下所示:

// Store a Nashorn script in a String object

String script = "print(msg)";

在语句中,脚本是:

print(msg)

注意msg是在print()函数调用中使用的变量。脚本没有声明msg变量,也没有给它赋值。如果你试图在不告诉引擎什么是msg变量的情况下执行上述脚本,引擎将抛出一个异常,声明它不理解变量msg的含义。这就是将参数从 Java 程序传递到脚本引擎的概念发挥作用的地方。

可以通过几种方式将参数传递给脚本引擎。最简单的方法是使用脚本引擎的put(String paramName, Object paramValue)方法,它接受两个参数:

  • 第一个参数是参数的名称,它需要与脚本中变量的名称相匹配。
  • 第二个参数是参数的值。

在您的例子中,您希望将一个名为msg的参数传递给脚本引擎,它的值是一个Stringput()的叫法是:

// Store the value of the msg parameter in the engine

engine.put("msg", "Hello from Java program");

注意,在调用eval()方法之前,必须先调用引擎的put()方法。在您的例子中,当引擎试图执行print(msg)时,它将使用您传递给引擎的msg参数的值。

大多数脚本引擎允许您使用传递给它的参数名作为脚本中的变量名。当您传递名为msg的参数值并在清单 2-3 的脚本中将它用作变量名时,您看到了这种例子。脚本引擎可能要求在脚本中声明变量,例如,在 PHP 中变量名必须以$前缀开头,而在 JRuby 中全局变量名包含$前缀。如果您想将名为msg的参数传递给 JRuby 中的脚本,您的代码应该如下所示:

// Get the JRuby script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("jruby");

// Must use the $ prefix in JRuby script

String script = "puts($msg)";

// No $ prefix used in passing the msg parameter to the JRuby engine

engine.put("msg", "Hello from Java");

// Execute the script

engine.eval(script);

传递给脚本的 Java 对象的属性和方法可以在脚本中访问,就像在 Java 代码中访问一样。不同的脚本语言使用不同的语法来访问脚本中的 Java 对象。例如,您可以在清单 2-3 所示的例子中使用表达式msg.toString(),输出将是相同的。在这种情况下,您正在调用变量msgtoString()方法。将清单 2-3 中赋值给script变量的语句改为如下,并运行程序,这将产生相同的输出:

String script = "println(msg.toString())";

从脚本向 Java 代码传递参数

脚本引擎可以使 Java 代码可以访问其全局范围内的变量。ScriptEngineget(String variableName)方法用于访问 Java 代码中的那些变量。它返回一个 Java Object。全局变量的声明依赖于脚本语言。以下代码片段声明了一个全局变量,并在 JavaScript 中为其赋值:

// Declare a variable named year in Nashorn

var year = 1969;

清单 2-4 包含了一个程序,展示了如何从 Java 代码中访问 Nashorn 中的一个全局变量。

清单 2-4。在 Java 代码中访问脚本全局变量

// AccessingScriptVariable.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class AccessingScriptVariable {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Write a script that declares a global variable named year

// and assign it a value of 1969.

String script = "var year = 1969";

try {

// Execute the script

engine.eval(script);

// Get the year global variable from the engine

Object year = engine.get("year");

// Print the class name and the value of the                         // variable year

System.out.println("year's class:" + year.                        getClass().getName());

System.out.println("year's value:" + year);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

year's class:java.lang.Integer

year's value:1969

程序在脚本中声明了一个全局变量year,并给它赋值 1969,如下所示:

String script = "var num = 1969";

当脚本执行时,引擎将year变量添加到它的状态中。在 Java 代码中,引擎的get()方法用于检索year变量的值,如下所示:

Object year = engine.get("year");

当脚本中声明了year变量时,您没有指定它的数据类型。脚本变量值到适当 Java 对象的转换是自动执行的。如果您在 Java 7 中运行程序,您的输出将显示java.lang.Double作为类名,1960.0 作为year变量的值。这是因为 Java 7 使用 Rhino 脚本引擎将 1969 年解释为Double,而 Java 8 使用 Nashorn 脚本引擎将其解释为Integer

摘要

一个ScriptEngine可以执行一个String和一个Reader中的脚本。使用ScriptEngineeval()方法执行脚本。该方法是重载的,它有六个版本。它允许您将脚本作为第一个参数传递给引擎,将参数作为第二个参数传递给引擎。

ScriptEngineeval()方法用于执行脚本。您可以将参数传递给脚本,并将脚本中的值读回 Java 程序。将参数从 Java 程序传递到脚本有不同的方式。您可以使用ScriptEngineput(String key, Object value)方法向脚本传递一个带有 name 键的参数。您可以使用get(String key)方法来获取保存在脚本中名为key的全局变量。

三、向脚本传递参数

在本章中,您将学习:

  • 用于从 Java 程序向脚本传递参数的类
  • 如何创建和使用Bindings对象来保存参数
  • 如何定义参数的范围
  • 如何使用不同的对象和范围向脚本传递参数
  • 不同参数传递方式的优缺点
  • 如何将脚本的输出写入文件

绑定、范围和上下文

为了理解参数传递机制的细节,必须清楚地理解三个术语:绑定、范围和上下文。这些术语起初令人困惑。本章使用以下步骤解释参数传递机制:

  • 首先,它定义了这些术语
  • 其次,它定义了这些术语之间的关系
  • 第三,它解释了如何在 Java 程序中使用它们

粘合剂

Bindings是一组键-值对,其中所有键必须是非空的非空字符串。在 Java 代码中,BindingsBindings接口的一个实例。SimpleBindings类是Bindings接口的一个实现。脚本引擎可以提供自己的Bindings接口实现。

Tip

如果你熟悉java.util.Map界面,就很容易理解BindingsBindings接口继承自Map<String,Object>接口。因此,Bindings只是一个Map,它的键必须是非空的非空字符串。

清单 3-1 显示了如何使用一个Bindings。它创建一个SimpleBindings的实例,添加一些键值对,检索键值,删除键值对,等等。Bindings接口的get()方法返回null,如果键不存在或者键存在且其值为null。如果你想测试一个键是否存在,你需要使用它的contains()方法。

清单 3-1。使用绑定对象

// BindingsTest.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.SimpleBindings;

public class BindingsTest {

public static void main(String[] args) {

// Create a Bindings instance

Bindings params = new SimpleBindings();

// Add some key-value pairs

params.put("msg", "Hello");

params.put("year", 1969);

// Get values

Object msg = params.get("msg");

Object year = params.get("year");

System.out.println("msg = " + msg);

System.out.println("year = " + year);

// Remove year from Bindings

params.remove("year");

year = params.get("year");

boolean containsYear = params.containsKey("year");

System.out.println("year = " + year);

System.out.println("params contains year = " + containsYear);

}

}

msg = Hello

year = 1969

year = null

params contains year = false

你不能单独使用一个Bindings。通常,您会使用它将参数从 Java 代码传递到脚本引擎。ScriptEngine接口包含一个返回Bindings接口实例的createBindings()方法。这个方法给脚本引擎一个机会来返回一个Bindings接口的特殊实现的实例。您可以使用这种方法,如下所示:

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Do not instantiate the SimpleBindings class, use the

// createBindings() method of the engine instead

Bindings params = engine.createBindings();

// Work with params as usual

范围

让我们转到下一个术语,即范围。一个作用域用于一个BindingsBindings的范围决定了它的键值对的可见性。您可以让多个Bindings出现在多个作用域中。但是,一个Bindings只能出现在一个作用域中。你如何指定一个Bindings的范围?我很快会谈到这一点。

使用Bindings的作用域可以让您按照层次顺序为脚本引擎定义参数变量。如果在引擎状态下搜索变量名,首先搜索优先级较高的Bindings,然后是优先级较低的Bindings。返回找到的第一个变量的值。

Java 脚本 API 定义了两个范围。它们在ScriptContext接口中被定义为两个int常量。它们是:

  • ScriptContext.ENGINE_SCOPE
  • ScriptContext.GLOBAL_SCOPE

引擎范围的优先级高于全局范围。如果将两个具有相同键的键-值对添加到两个Bindings(一个在引擎范围内,一个在全局范围内),每当必须解析与键同名的变量时,将使用引擎范围内的键-值对。

理解作用域对于一个Bindings的作用是如此重要,以至于我将通过另一个类比来解释它。考虑一个有两组变量的 Java 类:一组包含类中的所有实例变量,另一组包含方法中的所有局部变量。这两组变量及其值是两个BindingsBindings中变量的类型定义了作用域。为了便于讨论,我将定义两个范围:实例范围和本地范围。当执行一个方法时,首先在局部范围Bindings中查找变量名,因为局部变量优先于实例变量。如果在本地作用域Bindings中没有找到变量名,就在实例作用域Bindings中查找。当一个脚本被执行时,Bindings和它们的作用域扮演着相似的角色。

定义脚本上下文

脚本引擎在上下文中执行脚本。您可以将上下文视为脚本执行的环境。Java 宿主应用为脚本引擎提供了两样东西:脚本和脚本需要执行的上下文。接口的一个实例代表一个脚本的上下文。SimpleScriptContext类是ScriptContext接口的一个实现。脚本上下文由四部分组成:

  • 一组Bindings,其中每个Bindings与一个不同的作用域相关联
  • 脚本引擎用来读取输入的Reader
  • 脚本引擎用来写输出的一个Writer
  • 脚本引擎用来写入错误输出的错误Writer

上下文中的一组Bindings用于向脚本传递参数。上下文中的读取器和写入器分别控制脚本的输入源和输出目的地。例如,通过将文件编写器设置为编写器,可以将脚本的所有输出发送到文件。

每个脚本引擎都维护一个默认的脚本上下文,用于执行脚本。到目前为止,您已经在没有提供脚本上下文的情况下执行了几个脚本。在这些情况下,脚本引擎使用它们的默认脚本上下文来执行脚本。在这一节中,我将介绍如何单独使用ScriptContext接口的实例。在下一节中,我将介绍如何在脚本执行期间将一个ScriptContext接口的实例传递给一个ScriptEngine

您可以使用SimpleScriptContext类创建一个ScriptContext接口的实例,如下所示:

// Create a script context

ScriptContext ctx = new SimpleScriptContext();

一个SimpleScriptContext类的实例维护两个Bindings实例:一个用于引擎范围,一个用于全局范围。当您创建SimpleScriptContext的实例时,就会创建引擎范围内的Bindings。要使用全局范围Bindings,您需要创建一个Bindings接口的实例。

默认情况下,SimpleScriptContext类将上下文的输入读取器、输出写入器和错误写入器分别初始化为标准输入System.in、标准输出System.out和标准错误输出System.err。您可以使用ScriptContext接口的getReader()getWriter()getErrorWriter()方法分别从ScriptContext中获取阅读器、编写器和错误编写器的引用。还提供了 Setter 方法来设置读取器和编写器。下面的代码片段显示了如何获取阅读器和编写器。它还展示了如何将 writer 设置为FileWriter以将脚本输出写入文件:

// Get the reader and writers from the script context

Reader inputReader = ctx.getReader();

Writer outputWriter = ctx.getWriter();

Writer errWriter = ctx.getErrorWriter();

// Write all script outputs to an out.txt file

Writer fileWriter = new FileWriter("out.txt");

ctx.setWriter(fileWriter);

在创建了SimpleScriptContext之后,您可以开始在引擎范围Bindings中存储键值对,因为当您创建SimpleScriptContext对象时,在引擎范围中创建了一个空的BindingssetAttribute()方法用于向Bindings添加一个键值对。您必须为Bindings提供键名、值和范围。以下代码片段添加了三个键值对:

// Add three key-value pairs to the engine scope bindings

ctx.setAttribute("year", 1969, ScriptContext.ENGINE_SCOPE);

ctx.setAttribute("month", 9, ScriptContext.ENGINE_SCOPE);

ctx.setAttribute("day", 19, ScriptContext.ENGINE_SCOPE);

如果您想在全局范围内将键值对添加到一个Bindings中,您将需要首先创建并设置Bindings,如下所示:

// Add a global scope Bindings to the context

Bindings globalBindings = new SimpleBindings();

ctx.setBindings(globalBindings, ScriptContext.GLOBAL_SCOPE);

现在,您可以使用setAttribute()方法在全局范围内向Bindings添加键值对,如下所示:

// Add two key-value pairs to the global scope bindings

ctx.setAttribute("year", 1982, ScriptContext.GLOBAL_SCOPE);

ctx.setAttribute("name", "Boni", ScriptContext.GLOBAL_SCOPE);

此时,你可以看到ScriptContext实例的状态,如图 3-1 所示。

A978-1-4842-0713-0_3_Fig1_HTML.jpg

图 3-1。

A pictorial view of an instance of the SimpleScriptContext class

您可以在ScriptContext上执行多项操作。您可以使用setAttribute(String name, Object value, int scope)方法为已存储的密钥设置不同的值。对于指定的键和范围,可以使用removeAttribute(String name, int scope)方法移除键-值对。您可以使用getAttribute(String nameint scope)方法获取指定范围内的键值。

使用ScriptContext可以做的最有趣的事情是检索一个键值,而不用使用它的getAttribute(String name)方法指定它的作用域。一个ScriptContext首先在引擎范围Bindings中搜索关键字。如果在引擎范围内没有找到,则在全局范围内搜索Bindings。如果在这些范围中找到该键,则返回首先找到该键的范围中的相应值。如果两个范围都不包含该键,则返回null

在您的示例中,您已经在引擎范围和全局范围中存储了名为year的键。当首先搜索引擎范围时,下面的代码片段从引擎范围返回关键字year的 1969。getAttribute()方法的返回类型是Object

// Get the value of the key year without specifying the scope.

// It returns 1969 from the Bindings in the engine scope.

int yearValue = (Integer)ctx.getAttribute("year");

您只在全局范围内存储了名为name的键。如果尝试检索其值,将首先搜索引擎范围,这不会返回匹配项。随后,搜索全局范围并返回值"Boni",如下所示:

// Get the value of the key named name without specifying the scope.

// It returns "Boni" from the Bindings in the global scope.

String nameValue = (String)ctx.getAttribute("name");

您还可以检索特定范围内的键值。以下代码片段从引擎范围和全局范围中检索关键字"year"的值:

// Assigns 1969 to engineScopeYear and 1982 to globalScopeYear

int engineScopeYear = (Integer)ctx.getAttribute("year", ScriptContext.ENGINE_SCOPE);

int globalScopeYear = (Integer)ctx.getAttribute("year", ScriptContext.GLOBAL_SCOPE);

Tip

Java 脚本 API 只定义了两个作用域:引擎和全局。ScriptContext接口的子接口可以定义额外的作用域。ScriptContext接口的getScopes()方法返回一个支持范围的列表作为List<Integer>。请注意,作用域表示为整数。ScriptContext界面中的两个常量ENGINE_SCOPEGLOBAL_SCOPE分别被赋值为 100 和 200。当在出现在多个范围中的多个Bindings中搜索一个键时,首先搜索具有较小整数值的范围。因为引擎范围的值 100 小于全局范围的值 200,所以当您不指定范围时,首先在引擎范围中搜索一个键。

清单 3-2 展示了如何使用一个实现了ScriptContext接口的类的实例。请注意,您不能在应用中单独使用ScriptContext。它由脚本引擎在脚本执行期间使用。最常见的是,你通过一个ScriptEngine和一个ScriptEngineManager间接地操纵一个ScriptContext,这将在下一节详细讨论。

清单 3-2。使用 ScriptContext 接口的实例

// ScriptContextTest.java

package com.jdojo.script;

import java.util.List;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.SimpleBindings;

import javax.script.SimpleScriptContext;

import static javax.script.ScriptContext.ENGINE_SCOPE;

import static javax.script.ScriptContext.GLOBAL_SCOPE;

public class ScriptContextTest {

public static void main(String[] args) {

// Create a script context

ScriptContext ctx = new SimpleScriptContext();

// Get the list of scopes supported by the script context

List<Integer> scopes = ctx.getScopes();

System.out.println("Supported Scopes: " + scopes);

// Add three key-value pairs to the engine scope bindings

ctx.setAttribute("year", 1969, ENGINE_SCOPE);

ctx.setAttribute("month", 9, ENGINE_SCOPE);

ctx.setAttribute("day", 19, ENGINE_SCOPE);

// Add a global scope Bindings to the context

Bindings globalBindings = new SimpleBindings();

ctx.setBindings(globalBindings, GLOBAL_SCOPE);

// Add two key-value pairs to the global scope bindings

ctx.setAttribute("year", 1982, GLOBAL_SCOPE);

ctx.setAttribute("name", "Boni", GLOBAL_SCOPE);

// Get the value of year without specifying the scope

int yearValue = (Integer)ctx.getAttribute("year");

System.out.println("yearValue = " + yearValue);

// Get the value of name

String nameValue = (String)ctx.getAttribute("name");

System.out.println("nameValue = " + nameValue);

// Get the value of year from engine  and global scopes

int engineScopeYear = (Integer)ctx.getAttribute("year", ENGINE_SCOPE);

int globalScopeYear = (Integer)ctx.getAttribute("year", GLOBAL_SCOPE);

System.out.println("engineScopeYear = " + engineScopeYear);

System.out.println("globalScopeYear = " + globalScopeYear);

}

}

Supported Scopes: [100, 200]

yearValue = 1969

nameValue = Boni

engineScopeYear = 1969

globalScopeYear = 1982

把它们放在一起

在这一节中,我将向您展示Bindings的实例及其作用域、ScriptContextScriptEngineScriptEngineManager和宿主应用是如何协同工作的。重点将是如何使用一个ScriptEngine和一个ScriptEngineManager在不同的范围内操作存储在Bindings中的键值对。

一个ScriptEngineManager在一个Bindings中维护一组键值对。它允许您使用以下四种方法操作这些键值对:

  • void put(String key, Object value)
  • Object get(String key)
  • void setBindings(Bindings bindings)
  • Bindings getBindings()

put()方法向Bindings添加一个键值对。get()方法返回指定键的值;如果没有找到密钥,它返回null。使用setBindings()方法可以替换发动机管理器的BindingsgetBindings()方法返回ScriptEngineManagerBindings的引用。

默认情况下,每个ScriptEngine都有一个被称为默认上下文的ScriptContext。回想一下,除了读者和作者,一个ScriptContext有两个Bindings:一个在引擎范围内,一个在全局范围内。当一个ScriptEngine被创建时,它的引擎作用域Bindings为空,它的全局作用域Bindings引用创建它的ScriptEngineManagerBindings

默认情况下,由ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManagerBindings。在同一个 Java 应用中可能有多个ScriptEngineManager实例。在这种情况下,由同一个ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManagerBindings作为它们默认上下文的全局作用域Bindings

下面的代码片段创建了一个ScriptEngineManager,用于创建ScriptEngine的三个实例:

// Create a ScriptEngineManager

ScriptEngineManager manager = new ScriptEngineManager();

// Create three ScriptEngines using the same ScriptEngineManager

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

ScriptEngine engine3 = manager.getEngineByName("JavaScript");

现在,让我们给ScriptEngineManagerBindings添加三个键值对,给每个ScriptEngine的引擎范围Bindings添加两个键值对:

// Add three key-value pairs to the Bindings of the manager

manager.put("K1", "V1");

manager.put("K2", "V2");

manager.put("K3", "V3");

// Add two key-value pairs to each engine

engine1.put("KE11", "VE11");

engine1.put("KE12", "VE12");

engine2.put("KE21", "VE21");

engine2.put("KE22", "VE22");

engine3.put("KE31", "VE31");

engine3.put("KE32", "VE32");

图 3-2 显示了代码片段执行后ScriptEngineManager和三个ScriptEngine的状态。从图中可以明显看出,所有ScriptEngine的默认上下文共享ScriptEngineManagerBindings作为它们的全局作用域Bindings

A978-1-4842-0713-0_3_Fig2_HTML.jpg

图 3-2。

A pictorial view of three ScriptEngines created by a ScriptEngineManager

ScriptEngineManager中的Bindings可以通过以下方式修改:

  • 通过使用ScriptEngineManagerput()方法
  • 通过使用ScriptEngineManagergetBindings()方法获取Bindings的参考,然后在Bindings上使用put()remove()方法
  • 通过使用getBindings()方法在ScriptEngine的默认上下文的全局范围内获取Bindings的引用,然后在Bindings上使用put()remove()方法

当一个ScriptEngineManager中的Bindings被修改时,由这个ScriptEngineManager创建的所有ScriptEngine的默认上下文中的全局作用域Bindings被修改,因为它们共享同一个Bindings

每个ScriptEngine的默认上下文分别维护一个引擎范围Bindings。要将一个键值对添加到一个ScriptEngine的引擎作用域Bindings中,使用它的put()方法,如下所示:

ScriptEngine engine1 = null; // get an engine

// Add an "engineName" key with its value as "Engine-1" to the

// engine scope Bindings of the default context of engine1

engine1.put("engineName", "Engine-1");

ScriptEngineget(String key)方法从其引擎作用域Bindings返回指定的key的值。以下语句返回“Engine-1”,它是engineName键的值:

String eName = (String)engine1.get("engineName");

在默认的ScriptEngine上下文中,获得全局作用域Bindings的键值对需要两个步骤。首先,您需要使用它的getBindings()方法获得全局作用域Bindings的引用,如下所示:

Bindings e1Global = engine1.getBindings(ScriptContext.GLOBAL_SCOPE);

现在,您可以使用e1Global引用来修改引擎的全局范围Bindings。下面的语句向e1Global Bindings添加了一个键值对:

e1Global.put("id", 89999);

因为所有ScriptEngine共享一个ScriptEngine的全局作用域Bindings,这段代码将把键"id"和它的值添加到所有ScriptEngine的默认上下文的全局作用域Bindings中,所有ScriptEngine是由创建engine1的同一个ScriptEngineManager创建的。不建议使用此处所示的代码修改ScriptEngineManager中的Bindings。您应该改为使用ScriptEngineManager引用来修改Bindings,这使得代码的读者可以更清楚地理解逻辑。

清单 3-3 展示了本节讨论的概念。A ScriptEngineManager向它的Bindings添加两个键-值对,键为n1n2。创建了两个脚本引擎;他们在引擎范围Bindings中添加了一个名为engineName的键。当脚本被执行时,脚本中的engineName变量的值从ScriptEngine的引擎范围中被使用。脚本中变量n1n2的值是从ScriptEngine的全局作用域Bindings中获取的。在第一次执行该脚本后,每个ScriptEngine向它们的引擎范围Bindings添加一个名为n2的键,该键具有不同的值。当您第二次执行脚本时,变量n1的值从引擎的全局作用域Bindings中检索,而变量n2的值从引擎作用域Bindings中检索,如输出所示。

清单 3-3。使用由同一个 ScriptEngineManager 创建的引擎的全局和引擎范围绑定

// GlobalBindings.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class GlobalBindings {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

// Add two numbers to the Bindings of the manager that will be

// shared by all its engines

manager.put("n1", 100);

manager.put("n2", 200);

// Create two JavaScript engines and add the name of the engine

// in the engine scope of the default context of the engines

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

engine1.put("engineName", "Engine-1");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

engine2.put("engineName", "Engine-2");

// Execute a script that adds two numbers and prints the

// result

String script = "var sum = n1 + n2; "

+ "print(engineName + ' - Sum = ' + sum)";

try {

// Execute the script in two engines

engine1.eval(script);

engine2.eval(script);

// Now add a different value for n2 for each engine

engine1.put("n2", 1000);

engine2.put("n2", 2000);

// Execute the script in two engines again

engine1.eval(script);

engine2.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Engine-1 - Sum = 300

Engine-2 - Sum = 300

Engine-1 - Sum = 1100

Engine-2 - Sum = 2100

由一个ScriptEngineManager创建的所有 ScriptEngines 共享的全局作用域Bindings的故事还没有结束。这是最复杂、最令人困惑的事情!现在重点将放在使用ScriptEngineManager类的setBindings()方法和ScriptEngine接口的效果上。考虑以下代码片段:

// Create a ScriptEngineManager and two ScriptEngines

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

// Add two key-value pairs to the manager

manager.put("n1", 100);

manager.put("n2", 200);

图 3-3 显示了执行上述脚本后引擎管理器及其引擎的状态。此时,ScriptEngineManager中只存储了一个Bindings,两个脚本引擎将它作为它们的全局作用域Bindings来引用。

A978-1-4842-0713-0_3_Fig3_HTML.jpg

图 3-3。

Initial state of ScriptEngineManager and two ScriptEngines

让我们创建一个新的Bindings,并使用setBindings()方法将其设置为ScriptEngineManagerBindings,如下所示:

// Create a Bindings, add two key-value pairs to it, and set it as the new

// Bindings for the manager

Bindings newGlobal = new SimpleBindings();

newGlobal.put("n3", 300);

newGlobal.put("n4", 400);

manager.setBindings(newGlobal);

图 3-4 显示了代码执行后ScriptEngineManager和两个ScriptEngine的状态。请注意,ScriptEngineManager有了新的Bindings,而两个ScriptEngine仍然将旧的Bindings称为它们的全球范围Bindings

A978-1-4842-0713-0_3_Fig4_HTML.jpg

图 3-4。

State of ScriptEngineManager and two ScriptEngines after a new Bindings is set to the ScriptEngineManager

此时,对ScriptEngineManagerBindings所做的任何更改都不会反映在两个ScriptEngine的全局作用域Bindings中。您仍然可以对两个ScriptEngine共享的Bindings进行更改,并且两个ScriptEngine都将看到其中一个所做的更改。

让我们新建一个ScriptEngine,如图所示:

// Create a new ScriptEngine

ScriptEngine engine3 = manager.getEngineByName("JavaScript");

回想一下,ScriptEngine在创建时获得了全局作用域Bindings,BindingsScriptEngineManagerBindings相同。该语句执行后,ScriptEngineManager和三个 ScriptEngines 的状态如图 3-5 所示。

A978-1-4842-0713-0_3_Fig5_HTML.jpg

图 3-5。

State of ScriptEngineManager and three ScriptEngines after the third ScriptEngine is created

这里是另一个所谓的ScriptEngine s 的全局作用域的全球性的扭曲。这一次,您将使用一个ScriptEnginesetBindings()方法来设置它的全局作用域Bindings。图 3-6 显示了执行以下代码片段后ScriptEngineManager和三个脚本引擎的状态:

// Set a new Bindings for the global scope of engine1

Bindings newGlobalEngine1 = new SimpleBindings();

newGlobalEngine1.put("n5", 500);

newGlobalEngine1.put("n6", 600);

engine1.setBindings(newGlobalEngine1, ScriptContext.GLOBAL_SCOPE);

A978-1-4842-0713-0_3_Fig6_HTML.jpg

图 3-6。

State of ScriptEngineManager and Three ScriptEngines After a New Global Scope Bindings Is Set for engine1 Tip

默认情况下,一个ScriptEngineManager创建的所有脚本引擎共享它的Bindings作为它们的全局作用域Bindings。如果您使用一个ScriptEnginesetBindings()方法来设置它的全局作用域Bindings,或者如果您使用一个ScriptEngineManagersetBindings()方法来设置它的Bindings,您就打破了“全球性”链,如本节所讨论的。为了保持“全局”链的完整性,您应该总是使用ScriptEngineManagerput()方法将键值对添加到它的Bindings中。要从由ScriptEngineManager创建的所有 ScriptEngines 的全局范围中删除一个键-值对,您需要使用ScriptEngineManagergetBindings()方法获取Bindings的引用,并在Bindings上使用remove()方法。

使用自定义脚本上下文

在上一节中,您看到每个ScriptEngine都有一个默认的脚本上下文。ScriptEngineget()put()getBindings()setBindings()方法在默认ScriptContext下运行。当ScriptEngineeval()方法没有指定ScriptContext时,使用引擎的默认上下文。ScriptEngineeval()方法的以下两个版本使用其默认上下文来执行脚本:

  • Object eval(String script)
  • Object eval(Reader reader)

您可以将一个Bindings传递给下面两个版本的eval()方法:

  • Object eval(String script, Bindings bindings)
  • Object eval(Reader reader, Bindings bindings)

这些版本的eval()方法不使用默认的ScriptEngine上下文。他们使用一个新的ScriptContext,它的引擎作用域Bindings是传递给这些方法的,全局作用域Bindings与引擎的默认上下文相同。注意,eval()方法的这两个版本保持了ScriptEngine的默认上下文不变。

您可以将一个ScriptContext传递给下面两个版本的eval()方法:

  • Object eval(String script, ScriptContext context)
  • Object eval(Reader reader, ScriptContext context)

这些版本的eval()方法使用指定的上下文来执行脚本。它们保持ScriptEngine的默认上下文不变。

三组eval()方法允许您使用不同的隔离级别执行脚本:

  • 第一组让您共享所有脚本的默认上下文
  • 第二组让脚本使用不同的引擎作用域Bindings并共享全局作用域Bindings
  • 第三组让脚本在一个隔离的ScriptContext中执行

清单 3-4 展示了如何使用不同版本的eval()方法在不同的隔离级别执行脚本。该程序使用三个变量,分别叫做msgn1n2。它显示存储在msg变量中的值。将n1n2的值相加,并显示总和。该脚本打印出在计算总和时使用了什么值n1n2n1的值存储在由所有ScriptEngine的默认上下文共享的ScriptEngineManagerBindings中。n2的值存储在默认上下文和自定义上下文的引擎范围中。该脚本使用引擎的默认上下文执行两次,一次在开始,一次在结束,以证明在eval()方法中使用自定义BindingsScriptContext不会影响ScriptEngine的默认上下文中的Bindings。该程序在其main()方法中声明了一个throws子句,以使代码更短。

清单 3-4。使用不同的隔离级别执行脚本

// CustomContext.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

import javax.script.SimpleScriptContext;

import static javax.script.SimpleScriptContext.ENGINE_SCOPE;

import static javax.script.SimpleScriptContext.GLOBAL_SCOPE;

public class CustomContext {

public static void main(String[] args) throws ScriptException {

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Add n1 to Bindings of the manager, which will be shared

// by all engines as their global scope Bindings

manager.put("n1", 100);

// Prepare the script

String script = "var sum = n1 + n2;" +

"print(msg + " +

"' n1=' + n1 + ', n2=' + n2 + " +

"', sum=' + sum);";

// Add n2 to the engine scope of the default context of the

// engine

engine.put("n2", 200);

engine.put("msg", "Using the default context:");

engine.eval(script);

// Use a Bindings to execute the script

Bindings bindings = engine.createBindings();

bindings.put("n2", 300);

bindings.put("msg", "Using a Bindings:");

engine.eval(script, bindings);

// Use a ScriptContext to execute the script

ScriptContext ctx = new SimpleScriptContext();

Bindings ctxGlobalBindings = engine.createBindings();

ctx.setBindings(ctxGlobalBindings, GLOBAL_SCOPE);

ctx.setAttribute("n1", 400, GLOBAL_SCOPE);

ctx.setAttribute("n2", 500, ENGINE_SCOPE);

ctx.setAttribute("msg", "Using a ScriptContext:", ENGINE_SCOPE);

engine.eval(script, ctx);

// Execute the script again using the default context to

// prove that the default context is unaffected.

engine.eval(script);

}

}

Using the default context: n1=100, n2=200, sum=300

Using a Bindings: n1=100, n2=300, sum=400

Using a ScriptContext: n1=400, n2=500, sum=900

Using the default context: n1=100, n2=200, sum=300

eval()方法的返回值

ScriptEngineeval()方法返回一个Object,这是脚本中的最后一个值。如果脚本中没有最后一个值,它将返回null。依赖脚本中的最后一个值容易出错,同时也令人困惑。下面的代码片段展示了一些为 Nashorn 使用eval()方法返回值的例子。代码中的注释表示从eval()方法返回的值:

Object result = null;

// Assigns 3 to result because the last expression 1 + 2 evaluates to 3

result = engine.eval("1 + 2;");

// Assigns 7 to result because the last expression 3 + 4 evaluates to 7

result = engine.eval("1 + 2; 3 + 4;");

// Assigns 6 to result because the last statement v = 6 evaluates to 6

result = engine.eval("1 + 2; 3 + 4; var v = 5; v = 6;");

// Assigns 7 to result. The last statement "var v = 5" is a

// declaration and it does not evaluate to a value. So, the last

// evaluated value is 3 + 4 (=7).

result = engine.eval("1 + 2; 3 + 4; var v = 5;");

// Assigns null to result because the print() function returns undefined

// that is translated to null in Java.

result = engine.eval("print(1 + 2)");

最好不要依赖于eval()方法的返回值。您应该将一个 Java 对象作为参数传递给脚本,并让脚本将脚本的返回值存储在该对象中。在执行了eval()方法之后,您可以查询这个 Java 对象的返回值。清单 3-5 包含一个包装整数的Result类的代码。您将向脚本传递一个Result类的对象,脚本将在其中存储返回值。脚本完成后,您可以在 Java 代码中读取存储在 Result 对象中的整数值。需要将Result声明为公共的,以便脚本引擎可以访问它。清单 3-6 中的程序展示了如何将一个Result对象传递给一个用值填充Result对象的脚本。该程序在main()方法的声明中包含一个throws子句,以保持代码简短。

清单 3-5。包装整数的结果类

// Result.java

package com.jdojo.script;

public class Result {

private int val = -1;

public void setValue(int x) {

val = x;

}

public int getValue() {

return val;

}

}

清单 3-6。在结果对象中收集脚本的返回值

// ResultBearingScript.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ResultBearingScript {

public static void main(String[] args) throws ScriptException {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Pass a Result object to the script. The script will store the

// result of the script in the result object

Result result = new Result();

engine.put("result", result);

// Store the script in a String

String script = "3 + 4; result.setValue(101);";

// Execute the script, which uses the passed in

// Result object to return a value

engine.eval(script);

// Use the result object to get the returned value from // the script

int returnedValue = result.getValue(); // Will be 101

System.out.println("Returned value is " + returnedValue);

}

}

Returned value is 101

引擎范围绑定的保留键

通常,引擎范围Bindings中的一个键代表一个脚本变量。有些键是保留的,它们有特殊的含义。它们的值可以通过引擎的实现传递给引擎。一个实现可以定义附加的保留密钥。

表 3-1 包含所有保留密钥的列表。这些键在ScriptEngine接口中也被声明为常量。脚本引擎的实现不需要在引擎范围绑定中将所有这些键传递给引擎。作为开发人员,您不应该使用这些键将参数从 Java 应用传递到脚本引擎。

表 3-1。

The List of Reserved Keys for Engine Scope Bindings

钥匙 ScriptEngine 接口中的常数 键值的含义
" javax.script.argv " ScriptEngine.ARGV 用来传递一个数组对象来传递一组位置参数
" javax.script.engine " ScriptEngine.ENGINE 脚本引擎的名称
" javax.script.engine_version " ScriptEngine.ENGINE_VERSION 脚本引擎的版本
" javax.script.filename " ScriptEngine.FILENAME 用于传递文件或资源的名称,即脚本的来源
" javax.script.language " ScriptEngine.LANGUAGE 脚本引擎支持的语言的名称
" javax.script.language_version " ScriptEngine.LANGUAGE_VERSION 引擎支持的脚本语言版本
" javax.script.name " ScriptEngine.NAME 脚本语言的简称

更改默认脚本上下文

您可以分别使用getContext()setContext()方法来获取和设置ScriptEngine的默认上下文,如下所示:

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Get the default context of the ScriptEngine

ScriptContext defaultCtx = engine.getContext();

// Work with defaultCtx here

// Create a new context

ScriptContext ctx = new SimpleScriptContext();

// Configure ctx here

// Set ctx as the new default context for the engine

engine.setContext(ctx);

注意,为一个ScriptEngine设置一个新的默认上下文不会使用ScriptEngineManagerBindings作为它的全局作用域Bindings。如果您希望新的默认上下文使用ScriptEngineManagerBindings,您需要显式设置它,如下所示:

// Create a new context

ScriptContext ctx = new SimpleScriptContext();

// Set the global scope Bindings for ctx the same as the Bindings for

// the manager

ctx.setBindings(manager.getBindings(), ScriptContext.GLOBAL_SCOPE);

// Set ctx as the new default context for the engine

engine.setContext(ctx);

将脚本输出发送到文件

您可以自定义脚本执行的输入源、输出目标和错误输出目标。您需要为用于执行脚本的ScriptContext设置适当的读取器和写入器。下面的代码片段将把脚本输出写到当前目录中名为jsoutput.txt的文件中:

// Create a FileWriter

FileWriter writer = new FileWriter("jsoutput.txt");

// Get the default context of the engine

ScriptContext defaultCtx = engine.getContext();

// Set the output writer for the default context of the engine

defaultCtx.setWriter(writer);

该代码为ScriptEngine的默认上下文设置了一个自定义输出编写器,在使用默认上下文的脚本执行过程中将会用到这个编写器。如果您想使用定制的输出编写器来执行特定的脚本,您需要使用一个定制的ScriptContext并设置它的编写器。

Tip

ScriptContext设置自定义输出编写器不会影响 Java 应用标准输出的目的地。要重定向 Java 应用的标准输出,您需要使用System.setOut()方法。

清单 3-7 显示了如何将脚本执行的输出写到名为jsoutput.txt的文件中。该程序在标准输出中打印输出文件的完整路径。运行该程序时,您可能会得到不同的输出。您需要在文本编辑器中打开输出文件来查看脚本的输出。

清单 3-7。将脚本输出写入文件

// CustomScriptOutput.java

package com.jdojo.script;

import java.io.File;

import java.io.FileWriter;

import java.io.IOException;

import javax.script.ScriptContext;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class CustomScriptOutput {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Print the absolute path of the output file

File outputFile = new File("jsoutput.txt");

System.out.println("Script output will be written to " +

outputFile.getAbsolutePath());

FileWriter writer = null;

try {

writer = new FileWriter(outputFile);

// Set a custom output writer for the engine

ScriptContext defaultCtx = engine.getContext();

defaultCtx.setWriter(writer);

// Execute a script

String script =                                  "print('Hello custom output writer')";

engine.eval(script);

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

finally {

if (writer != null) {

try {

writer.close();

}

catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

Script output will be written to C:\jsoutput.txt

摘要

您可以使用ScriptContext将参数传递给脚本。Bindings接口的一个实例充当参数持有者。Bindings接口从Map接口继承而来,施加了一个限制,即它的键必须是非空的、非空的StringSimpleBinding类是Bindings接口的一个实现。最好使用ScriptEnginecreateBindings()方法来获取Bindings接口的实例,给ScriptEngine一个机会返回Bindings接口的特定实现。Bindings与一个作用域相关联,该作用域可以是引擎作用域或全局作用域。在引擎范围内传递的参数优先于在全局范围内传递的参数。传递的参数可以是脚本引擎的本地参数、脚本执行的本地参数或由ScriptManager创建的所有脚本引擎的全局参数。

脚本引擎在由四个组件组成的上下文中执行脚本:一组Bindings,其中每个Bindings与不同的范围相关联;一个Reader,由脚本引擎用来读取输入;一个Writer,由脚本引擎用来写入输出;以及一个错误Writer,由脚本引擎用来写入错误输出。ScriptContext接口的一个实例表示脚本执行的上下文。SimpleScriptContext类是ScriptContext接口的一个实现。

上下文中的一组Bindings用于向脚本传递参数。上下文中的读取器和写入器分别控制脚本的输入源和输出目的地。例如,通过将文件编写器设置为编写器,可以将脚本的所有输出发送到文件。每个ScriptEngine都有一个默认的ScriptContext,用于在没有ScriptContext被传递给eval()方法时执行脚本。ScriptEnginegetContext()方法返回引擎的默认上下文。您还可以通过eval()方法传递一个单独的ScriptContext,该方法将用于执行您的脚本,而不改变引擎的默认ScriptContext

四、使用 Nashorn 编写脚本

在本章中,您将学习:

  • 如何用 Nashorn 编写脚本
  • 在严格模式和非严格模式下运行脚本的区别
  • 如何声明变量和编写注释
  • 基元和对象数据类型,以及如何将一种数据类型的值转换为另一种数据类型
  • Nashorn 中的运算符、语句以及创建和调用函数
  • 创建和使用对象的不同方式
  • 变量范围和提升
  • 关于内置全局对象和函数

Nashorn 是 JVM 上 ECMAScript 5.1 规范的运行时实现。ECMAScript 定义了自己的语法和构造,用于声明变量、编写语句、操作符、创建和使用对象、集合、迭代数据集合等等。Nashorn 100%符合 ECMAScript 5.1,因此在使用 Nashorn 时,您需要学习一套新的语言语法。例如,在 Nashorn 中处理对象与在 Java 中处理对象完全不同。

本章假设您对 Java 编程语言至少有初级的理解。我不会详细解释 Nashorn 提供的结构和语法,它们在 Java 中的工作方式是一样的。例如,我不会解释 Nashorn 中的赋值操作符=是做什么的。相反,我将简单地提到赋值操作符=在 Nashorn 和 Java 中是一样的。Nashorn 的强大之处在于它允许您在脚本中使用 Java 库。然而,要在 Nashorn 中使用 Java 库,您必须知道 Nashorn 的语法和结构。本章简要介绍了 Nashorn 的语法和结构。

严格和非严格模式

Nashorn 可以在两种模式下运行:严格模式和非严格模式。ECMAScript 的某些功能不能在严格模式下使用。通常,在严格模式下不允许使用容易出错的功能。一些在非严格模式下工作的功能在严格模式下会产生错误。我会在解释具体特性的同时,列出适用于严格模式的特性。您可以通过两种方式在脚本中启用严格模式:

  • 使用带有jjs命令的–strict选项
  • 使用"use strict"'use strict'指令

以下命令在严格模式下调用jjs命令行工具,并试图在不声明变量的情况下将值 10 赋给名为empId的变量。您会收到一条错误消息,指出变量empId未定义:

C:\> jjs -strict

jjs> empId = 10;

<shell>:1 ReferenceError: "empId" is not defined

jjs> exit()

解决方案是使用关键字var(稍后讨论)在严格模式下声明empId变量。

下面的命令在非严格模式下调用jjs命令行工具(没有–strict选项),并试图在不声明变量的情况下给名为empId的变量赋值 10。这一次,您不会收到错误;相反,变量的值(即10)被打印出来:

C:\> jjs

jjs> empId = 10;

10

jjs>exit()

清单 4-1 显示了一个使用 use strict 指令的脚本。该指令在脚本或函数的开头指定。use strict 指令就是字符串"use strict"。您也可以像'use strict'一样用单引号将指令括起来。该脚本在没有声明变量的情况下为变量赋值,这将产生一个错误,因为启用了严格模式。

清单 4-1。带有严格模式指令的脚本

// strict.js

"use strict"; // This is the use strict directive.

empId = 10;   // This will generate an error.

标识符

标识符是脚本中变量、函数、标签等的名称。Nashorn 中的标识符是一个 Unicode 字符序列,其规则如下:

  • 它可以包含字母、数字、下划线和美元符号
  • 它不能以数字开头
  • 它不能是保留字之一
  • 标识符中的字符可以替换为 Unicode 转义序列,其形式为\uxxxx,其中xxxx是十六进制格式的字符的 Unicode 数值

以下是有效标识符的示例:

  • empId
  • emp_id
  • _empId
  • emp$Id
  • num1
  • \u0061bc(与abc相同,因为\u0061是字符a的 Unicode 转义序列)

以下是无效标识符的示例:

  • 4num(不能以数字开头)
  • emp id(不能包含空格)
  • emp+id(不能包含+号)
  • break ( break是保留字,不能用作标识符)

表 4-1 、 4-2 和 4-3 列出了 Nashorn 中的保留字。表 4-1 中列出的保留字已经被用作关键字。您对 Java 中的大多数关键字都很熟悉。在纳顺,它们有相同的意义;例如,fordowhile用于表示循环结构,而breakcontinue用于中断循环并继续循环中的下一次迭代。我将在这一章简单解释一下 Nashorn 特有的关键词。表 4-2 和 4-3 列出了现在还没有使用的关键字,但是以后会用到。

使用任何保留字作为标识符都会产生错误。表 4-3 中的保留字仅在严格模式下会产生错误。它们可以在非严格模式下使用,没有任何错误。

表 4-3。

The List of Future Reserved Words in Strict-Mode in Nashorn

implements let private public
yield interface package protected
static

表 4-2。

The List of Future Reserved Words in Nashorn

class enum extends super
const export import

表 4-1。

The List of Reserved Words in Nashorn Used as Keywords

break do instanceof typeof
case else new var
catch finally return void
continue for switch while
debugger function this with
default if throw
delete in try

评论

Nashorn 支持两种类型的注释:

  • 单行注释
  • 多行注释

在 Nashorn 中编写注释的语法与 Java 相同。以下是注释的示例:

// Let us declare a variable named empId (A single-line comment)

var empId;

/* Let us declare a variable named empList

and another variable named deptId (A multi-line comment)

*/

var empList;

var deptId;

声明变量

脚本语言是松散类型的。变量的类型在编译时是未知的。在程序执行期间,变量的类型可以改变。变量的类型是在运行时根据变量中存储的值确定的。例如,同一个变量可以在某处存储一个数字,在另一处存储一个字符串。Nashorn 中的这条规则与 Java 有很大的不同,Java 是一种强类型语言,变量的类型在其声明中是已知的。

在 Nashorn 中,关键字var用于声明一个变量。在 ECMAScript 术语中,变量声明被称为变量语句:

// Declare a variable named msg

var msg;

注意,与 Java 不同的是,在 Nashorn 中不需要指定声明变量的数据类型。您可以在一个变量语句中声明多个变量。变量名由逗号分隔:

// Declare three variables

var empId, deptId, emplList;

Tip

在严格模式下,将变量命名为evalarguments都是错误的。

变量在声明时被初始化为undefined。我将在下一节讨论数据类型和undefined值。也可以在声明时用值初始化变量:

/* Declare and initialize variables deptId, and empList. deptId is

initialized to the number 400 and empList is initialized to an

array of strings.

*/

var deptId = 400, emplList = ["Ken", "Lydia", "Simon"];

请注意,您可以使用数组文字在 Nashorn 中创建一个数组,该数组文字包含用括号括起来的逗号分隔的数组元素列表。我将在第七章中详细讨论数组。

在非严格模式下,可以在变量声明中省略关键字var:

// Declare a variable named greeting without using the keyword var

greeting = "Hello";

数据类型

中的数据类型可以分为两类:基元类型和对象类型。基本类型包括以下五种数据类型:

  • 不明确的
  • 数字
  • 布尔代数学体系的
  • 线

未定义的类型

未定义的类型只有一个名为undefined的值。Nashorn 中已声明但已赋值的变量的值为undefined。你也可以显式地给一个变量赋值undefined。另外,你可以用undefined比较另一个值。下面的代码片段显示了如何使用值undefined:

// empId is initialized to undefined implicitly

var empId;

// deptId is initilaized to undefined explicitly

var deptId = undefined;

// Print the values of empId and deptId

print("empId is", empId)

print("deptId is", deptId);

if (empId == undefined) {

print("empId is undefined")

}

if (deptId == undefined) {

print("deptId is undefined")

}

empId is undefined

deptId is undefined

empId is undefined

deptId is undefined

空类型

空类型只有一个名为null的值。尽管值null被认为是原始类型,但它通常用在需要一个对象但没有有效对象要指定的地方。下面的代码片段显示了如何使用值null:

var person = null;

print("person is", person);

person is null

数字类型

与 Java 不同,Nashorn 不区分整数和浮点数。它只有一种称为 Number 的类型来表示这两种类型的数值。数字以双精度 64 位 IEEE 浮点格式存储。所有的数字常量都称为数字文字。像 Java 一样,您可以用十进制、十六进制、八进制和科学记数法来表示数字文字。Nashorn 定义了数字类型的三个特殊值:非数字、正无穷大和负无穷大。在脚本中,这些特殊值分别由NaN、+ Infinity–Infinity表示。正无穷大值也可以简单地表示为Infinity,没有前导的+符号。以下代码片段显示了如何使用数字文字和特殊数字类型值:

var empId = 100;               // An integer of type Number

var salary = 1500.678;         // A lfoating-point number of type Number

var hexNumber = 0x0061;        // Same as 97 is decimal

var octalNumber = 0141;        // Same 97 in decimal

var scientificNumber = 0.97E2; // Same 97 in decimal

var notANumber = NaN;

var posInfinity = Infinity;

var negInfinity = -Infinity;

// Print all values

print("empId =", empId);

print("salary =", salary);

print("hexNumber =", hexNumber);

print("octalNumber =", octalNumber);

print("scientificNumber =", scientificNumber);

print("notANumber =", notANumber);

print("posInfinity =", posInfinity);

print("negInfinity =", negInfinity);

empId = 100

salary = 1500.678

hexNumber = 97

octalNumber = 97

scientificNumber = 97

notANumber = NaN

posInfinity = Infinity

negInfinity = - Infinity

布尔类型

布尔类型代表一个逻辑实体,它或者为真或者为假。像 Java 一样,Nashorn 有两个布尔类型的文字,truefalse:

Var isProcessing = true;

var isProcessed = false;

print("isProcessing =", isProcessing);

print("isProcessed =", isProcessed);

isProcessing = true

isProcessed = false

字符串类型

字符串类型包括零个或多个 Unicode 字符的所有有限有序序列。用双引号或单引号括起来的字符序列称为字符串文字。序列中的字符数称为字符串的长度。以下是使用字符串文字的示例:

var greetings = "Hi there";     // A string literal of length 8

var title = 'Scripting in Java'; // A string literal enclosed in single quotes

var emptyMsg = "";              // An empty string of length zero

如果字符串文字用双引号括起来,它可以包含单引号,反之亦然。如果字符串文字用双引号括起来,如果想在字符串中包含双引号,就需要用反斜杠对双引号进行转义。单引号中的字符串也是如此。以下代码片段向您展示了一些示例:

var msg1 = "It's here and now.";

var msg2 = 'He said, "He is happy."';

var msg3 = 'It\'s here and now.';

var msg4 = "He said, \"He is happy.\"";

print(msg1);

print(msg2);

print(msg3);

print(msg4);

It's here and now.

He said, "He is happy."

It's here and now.

He said, "He is happy."

与 Java 不同,Nashorn 中的字符串文字可以写成多行。您需要在行尾使用反斜杠作为延续字符。注意反斜杠和行结束符不是字符串的一部分。下面是一个用三行代码编写字符串文本Hello World!的示例:

// Assigns the string, Hello world!, to msg using a multi-line string literal

var msg = "Hello \

world\

!";

print(msg);

Hello world!

如果想在多行字符串中插入换行符,需要使用转义序列\n,如下所示。请注意,我将开头和结尾的引号放在了单独的一行上,这使得多行文本更加易读:

// Uses a multi-line string with embedded newlines

var lucyPoem = "\

STRANGE fits of passion have I known:\n\

And I will dare to tell,\n\

But in the lover's ear alone,\n\

What once to me befell.\

";

print(lucyPoem);

STRANGE fits of passion have I known:

And I will dare to tell,

But in the lover's ear alone,

What once to me befell.

字符串中的字符可以按字面意思出现,也可以以转义序列的形式出现。您可以使用 Unicode 转义序列来表示任何字符。某些字符(如行分隔符、回车符等)不能按字面意思表示,它们必须以转义序列的形式出现。表 4-4 列出了 Nashorn 中定义的转义序列。

表 4-4。

Single Character Escape Sequences in Nashorn

字符转义序列 Unicode 转义序列 字符名称
\b \u0008 退格键
\t \u0009 横表
\n \ u000A 换行(新行)
\v \u000B 垂直标签
\f \ u000C 换页
\r \u000D 回车
" \u0022 双引号
' \u0027 单引号
\ \u005C 反斜线符号

Nashorn 和 Java 解释 Unicode 转义序列的方式有很大的不同。与 Java 不同,Nashorn 在执行代码之前不会将 Unicode 转义序列解释为实际字符。Java 中的以下单行注释、字符文字和字符串文字将不会编译,因为编译器会在编译程序之前将\u000A转换为新的一行:

// This, \u000A, is a new line, making the single line comment invalid

char c = '\u000A';

String str = "Hello\u000Aworld!";

在 Java 中,你必须将代码中的\u000A替换为\n才能工作。Nashorn 中的以下代码片段工作正常:

// This, \u000A, is a new line that is valid in Nashorn

var str = "Hello\u000AWorld!";

print(str);

Hello

World!

我将推迟对对象类型的讨论,直到我解释完 Nashorn 中的基本构造,比如操作符、语句、循环等等。

经营者

Nashorn 支持许多运营商。大部分和 Java 操作符一样。我将在这一节列出所有的操作符,并在这里讨论其中的几个。我将在后续章节中适当的时候讨论其他问题。表 4-5 列出了 Nashorn 的操作员。

表 4-5。

The List of Operators in Nashorn

操作员 名字 句法 描述
++ 增量 ++i i++ 将操作数增加 1。
-- 减量 --i i-- 将操作数减 1。
delete delete prop 从对象中删除指定的属性。
void void expr 丢弃指定表达式的返回值。
typeof typeof expr 返回描述指定表达式类型的字符串。
+ 一元加号 +op 将操作数转换为数字类型。
- 一元否定 -op 将操作数转换为数字类型,然后对转换后的值求反。
按位非 ∼op 将操作数用作 32 位有符号整数,翻转其位,并将结果作为 32 位有符号整数返回。
! 逻辑非 !expr 如果expr的计算结果为false,则返回true。如果expr评估为true,则返回false
+ 数字加法/字符串串联 op1 + op2 如果其中一个操作数是字符串或者它们可以转换为字符串,则执行字符串串联。否则,执行数字加法。
- 减法 op1 - op2 对两个操作数执行数字减法,如果它们不是数字,则将它们转换为数字。
/ 分开 op1 / op2 执行除法并返回两个操作数的商。
* 增加 op1 * op2 执行乘法并返回操作数的乘积。
% 剩余物 op1 % op2 使用左操作数作为除法运算的被除数,右操作数作为除数,并返回余数。
in prop in obj 如果obj包含名为prop的属性,则返回true。否则,返回 false。这里,prop是字符串或可转换为字符串的值,obj是对象。
instanceof obj instanceof cls obj的返回true是类cls的一个实例。否则,返回false
< 小于 op1 < op2 op1的返回值true小于op2。否则,返回false
<= 小于或等于 op1 <= op2 op1的返回值true小于或等于op2。否则,返回false
> 大于 op1 > op2 op1的返回值true大于op2。否则,返回false
>= 大于或等于 op1 >= op2 op1的返回值true大于等于op2。否则,返回false
== 平等 op1 == op2 如果op1op2相等,则返回true。否则,返回false。如有必要,将应用类型转换。例如,"2" == 2返回true,因为字符串"2"被转换为数字2,然后两个操作数相等。
!= 不平等 op1 != op2 如果op1op2不相等,则返回true。否则,返回false。如有必要,将应用类型转换。
=== 同一性或严格平等 op1 === op2 如果两个操作数的类型和值相同,则返回true。否则,返回false。表达式,"2" === 2返回的false "2"是一个字符串,2是一个数字。表达式"2" === "2"2 === 2都返回true
!== 非同一性或严格不平等 op1 !== op2 如果操作数不相等和/或不属于同一类型,则返回true。两个表达式"2" !== 22 !==3都返回true。在第一个表达式中,操作数的类型不匹配(字符串和数字),在第二个表达式中,操作数的值不匹配。
<< 按位左移 op1 << op2 按照op2指定的量对op1执行按位左移操作。在执行左移之前,op1被转换为 32 位有符号整数。结果也是一个 32 位有符号整数。
>> 按位带符号右移 op1 >> op2 按照op2指定的量对op1执行符号填充按位右移操作。左边的新位填充了保持op1符号和结果相同的原始最高有效位。将op1转换为 32 位有符号整数,结果也是 32 位有符号整数。
>>> 按位无符号右移 op1 >>> op2 按照op2指定的量对op1执行补零按位右移操作。左边的新位用零填充,使结果成为无符号 32 位整数。
& 按位“与” op1 & op2 op1op2的每一对位执行按位 AND 运算。如果两位都为 1,则结果位为 1。否则,结果位为 0。
&#124; 按位或 op1 &#124; op2 op1op2的每一对位执行按位“或”运算。如果任一位为 1,则结果位为 1。如果两位都为 0,则结果位为 0。
^ 按位异或 op1 ^ op2 op1op2的每一对位执行逐位异或运算。如果位不同,则结果位为 1。否则,结果位为 0。
&& 逻辑“与” op1 && op2 返回op1op2。如果op1false或者可以转换为false,则返回op1。否则,返回op2
&#124;&#124; 逻辑或 op1 &#124;&#124; op2 返回op1op2。如果op1true或者可以转换为true,则返回op1。否则,返回op2
?: 条件或三元运算符 op1 ? op2 : op3 如果op1评估为真,则返回 op2 的值。否则,返回 op3 的值。
= 分配 op1 = op2 op2的值赋给op1并返回op2
+=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, ^=, &#124;= 复合赋值 op1 op= op2 如同执行类似op1 = op1 op op2的语句一样工作。应用运算(+、=、*、/、%)上的op1op2,将结果赋给 op1 并返回结果。
, 逗点算符 op1, op2, op3... 从左到右计算每个操作数,并返回最后一个操作数的值。用于需要一个表达式,但你想使用多个表达式的地方,例如在for循环头中。

运算符具有优先权。在表达式中,优先级较高的运算符在优先级较低的运算符之前计算。像 Java 一样,可以将表达式中优先级最高的部分括在括号中。以下是运算符的优先级列表。级别 1 的操作员比级别 2 的操作员具有更高的优先级。同一级别的运算符具有相同的优先级:

++ (Postfix increment), -- (postfix decrement)   !, ∼, + (Unary plus), - (Unary negation), ++ (Prefix increment), -- (Prefix decrement), typeof, void, delete   *, /, %   + (Addition), - (Subtraction)   << , >>, >>>   <, <=, >, >=, in, instanceof   ==, !=, ===, !==   &   ^   |   &&   ||   ? :   =, +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=,  ^=,  |=   , (Comma operator)

我将在这一节讨论几个操作符。Nashorn 中的这些操作符要么不是 Java 语言,要么工作方式非常不同:

  • 相等运算符(==)和严格相等运算符(===)
  • 逻辑 AND ( &&)和逻辑 OR ( ||)运算符

==操作符的工作方式与它在 Java 中的工作方式几乎相同。它检查两个操作数是否相等,如果可能的话,执行诸如字符串到数字的类型转换。例如,"2" == 2返回true,因为字符串"2"被转换为数字 2,然后两个操作数相等。表达式2 == 2也返回true,因为两个操作数都是 number 类型,并且它们的值相等。相比之下,===操作符检查两个操作数的类型和值是否相等。如果两者不相同,则返回false。例如,“2”= = = 2 返回false,因为一个操作数是字符串,另一个是数字。它们的类型不匹配,即使它们的值在转换为字符串或数字时匹配。

在 Java 中,&&||操作符处理布尔操作数,它们返回truefalse。在纳顺,情况并非如此;这些运算符返回任意类型的操作数值之一。在 Nashorn 中,任何类型的值都可以转换成布尔值truefalse。可以转换成true的值称为 truthy,可以转换成false的值称为 falsy。我将在本章的下一节提供真值和假值的完整列表。

&&||运算符处理 truthy 和 falsy 操作数,并返回一个不一定是布尔值的操作数值。&&||运算符也称为短路运算符,因为如果第一个操作数本身可以确定结果,它们不会计算第二个操作数。

如果第一个操作数为 falsy,则&&操作符返回该操作数。否则,它返回第二个操作数。考虑以下语句:

var result = true && 120; // Assigns 120 to result

该语句将 120 分配给result&&的第一个操作数为真,所以它对第二个操作数求值并返回。考虑另一种说法:

var result = false && 120; // Assigns false to result

该语句将false赋给结果。&&的第一个操作数是false,所以它不计算第二个操作数。它只是返回第一个操作数。

如果第一个操作数为真,则||运算符返回该操作数。否则,它返回第二个操作数。考虑以下语句:

var result = true || 120; // Assigns true to result

该语句将 true 赋值给result||的第一个操作数是true,所以它返回第一个操作数。考虑另一种说法:

var result = false || 120; // Assigns 120 to result

该语句将120赋给结果。||的第一个操作数是false,因此它计算第二个操作数并返回其值。

类型变换

Java 中不允许的事情在 Nashorn 中是允许的,比如在需要布尔值的地方使用数字或字符串。考虑 Nashorn 中的以下代码片段,它将一个布尔值添加到一个数字中:

var n1 = true + 120;

var n2 = false + 120;

print("n1 = " + n1);

print("n2 = " + n2);

n1 = 121

n2 = 120

Java 中不允许使用表达式true + 120。然而,在纳斯雄是允许的。注意,纳斯霍恩隐式地将true转换为数字 1,将false转换为数字 0。Nashorn 执行了大量隐式转换。您需要对它们有很好的理解,才能在 Nashorn 中编写无 bug 的代码。以下部分详细解释了这些转换。

到布尔转换

在 Nashorn 中,只要需要布尔值,就可以使用 true 或 falsy 值。例如,if语句中的条件不需要产生布尔值。它可以是任何真值或假值。表 4-6 列出了值的类型及其对应的转换布尔值。

表 4-6。

The List of Value Types and Their Corresponding Converted Boolean Values

值类型 转换的布尔值
Undefined false
Null false
Boolean 身份转换
Number 如果参数为+0-0NaN,则计算结果为false;否则,评估为true
String 空字符串的计算结果为false。所有其他字符串评估为true
Object true

您还可以使用Boolean()全局函数来显式地将一个值转换为布尔类型。该函数将待转换的值作为参数。以下代码片段包含一些隐式和显式转换为布尔类型的示例:

var result;

result = undefined ? "undefined is truthy" : "undefined is falsy";

print(result);

result = null ? "null is truthy" : "null is falsy";

print(result);

result = 100 ? "100 is truthy" : "100 is falsy";

print(result);

result = 0 ? "0 is truthy" : "0 is falsy";

print(result);

result = Boolean("") ? "The empty string is truthy"

: "The empty string is falsy";

print(result);

result = 'Hello' ? "'Hello' is truthy" : "'Hello' is falsy";

print(result);

undefined is falsy

null is falsy

100 is truthy

0 is falsy

The empty string is falsy

'Hello' is truthy

至数字转换

Nashorn 中所有类型的值都可以隐式或显式地转换为 Number 类型。表 4-7 列出了数值的类型及其对应的转换数值。

表 4-7。

The List of Value Types and Their Corresponding Converted Numeric Values

值类型 转换后的数值
Undefined NaN
Null +0
Boolean 布尔值true被转换为 1,而false被转换为 0
Number 身份转换
String 空字符串和只包含空格的字符串被转换为零。一个字符串,其内容在修剪了前导空格和尾随空格后可被解释为数字,该字符串被转换为相应的数值。如果字符串的数字内容过大或过小,则分别转换为+Infinity–Infinity。所有其他字符串都转换为NaN
Object 如果对象的内容可以解释为数字,则对象被转换为相应的数值;否则,对象被转换为NaN

您可以使用Number()全局函数显式地将一个值转换为数字类型。该函数将要转换的值作为参数,并返回一个数字。以下代码片段包含一些隐式和显式转换为数字类型的示例:

var result;

result = Number(undefined);

print("undefined is converted to", result);

// Any number + NaN is NaN

result = 10 + undefined;

print("10 + undefined is", result);

result = Number("");

print("The empty string is converted to", result);

result = Number('Hello');

print("'Hello' is converted to", result);

// Convertes to the number 1982, ignoring leading and trailing whitespaces

result = Number(' 1982 ');

print("' 1982 ' is converted to", result);

result = Number(new Object(88));

print("new Object(88) is converted to", result);

result = Number(new Object());

print("new Object() is converted to", result);

// A very big number in a string

result = Number("10E2000");

print("10E2000 is converted to", result);

undefined is converted to NaN

10 + undefined is NaN

The empty string is converted to 0

'Hello' is converted to NaN

' 1982 ' is converted to 1982

new Object(88) is converted to 88

new Object() is converted to NaN

10E2000 is converted to Infinity

到字符串转换

Nashorn 中所有类型的值都可以隐式或显式地转换为 String 类型。表 4-8 列出了值的类型及其对应的转换后的字符串值。

表 4-8。

The List of Value Types and Their Corresponding Converted String Values

值类型 转换后的字符串值
Undefined "undefined"
Null "null"
Boolean 布尔值true被转换为“真”,而false被转换为“假”
Number +00-0转换为“0”;NaN转换为“南”;Infinity(或+Infinity)转换为“无穷大”;-Infinity转换为“-无穷大”。所有其他数字都转换为相应的十进制或科学记数法的字符串表示形式。较大的数字可能无法准确转换
String 身份转换
Object 如果对象是原始值的包装器,它将原始值作为字符串返回;否则,通过调用toString()方法返回对象的字符串表示。如果对象中不存在toString()方法,则返回valueOf()方法返回值的字符串表示

您可以使用String()全局函数显式地将一个值转换为字符串类型。该函数将要转换的值作为参数,并返回一个字符串。以下代码片段包含一些隐式和显式转换为字符串类型的示例:

var result;

result = String(undefined);

print("undefined is converted to", result);

result = String(true);

print("true is converted to", result);

result = String(9088);

print("9088 is converted to", result);

result = String(0x786A);

print("0x786A is converted to", result);

result = String(900000000000000000000);

print("900000000000000000000 is converted to", result);

result = String(9000000000000000000000);

print("9000000000000000000000 is converted to", result);

result = String(new Object(1982));

print("new Object(1982) is converted to", result);

result = String(new Object());

print("new Object() is converted to", result);

undefined is converted to undefined

true is converted to true

9088 is converted to 9088

0x786A is converted to 30826

900000000000000000000 is converted to 900000000000000000000

9000000000000000000000 is converted to 9e+21

new Object(1982) is converted to 1982

new Object() is converted to [object Object]

声明

Nashorn 包含了 Java 中的大部分语句。大多数语句类型的工作方式与 Java 非常相似。以下是 Nashorn 中的语句类型列表:

  • 块语句
  • 可变语句
  • 空白语句
  • 表达式语句
  • if声明
  • 迭代(或循环)语句
  • continue声明
  • break声明
  • return声明
  • with声明
  • switch声明
  • 带标签的陈述
  • throw声明
  • try声明
  • 调试器语句

和 Java 一样,你可以用分号来结束一个语句。然而,与 Java 不同,语句结束符是可选的。Nashorn 会在很多情况下插入分号。大多数情况下,您可以省略分号作为语句结束符,没有任何问题。Nashorn 会根据需要自动插入它们。在某些情况下,分号的自动插入可能会导致程序的错误解释。以下规则用于自动插入分号:

  • 如果解析器遇到当前正在解析的语句中不允许的源文本,并且该文本前面至少有一个行结束符,则会自动插入分号
  • 如果语句后有右大括号(}),则在语句后插入分号
  • 在程序的末尾插入一个分号
  • 如果分号将被解析为空语句,它不会自动插入(我将在本章后面解释空语句)。分号也不会插入到for语句的头部

考虑以下代码:

var x = 1, y = 3, z = 5

x = y

z++

printf("x = %d, y = %d, z = %d", x, y, z)

解析器将自动插入分号作为语句终止符,就像您编写了如下代码一样:

var x = 1, y = 3, z = 5;

x = y;

z++;

printf("x = %d, y = %d, z = %d", x, y, z);

考虑以下代码:

var x = 10, y = 20

if (x > y)

else x = y

解析器会在变量语句和赋值语句x = y后插入分号,但不会在if语句后插入分号。转换后的代码如下:

var x = 10, y = 20;

if (x > y)

else x = y;

分号不会自动插入到if语句之后,因为插入的分号将被解释为空语句。这段代码将无法编译,因为if语句有一个条件,但缺少一个语句。

解析器一直解析源代码,直到找到一个有问题的标记。它并不总是在行结束符前插入分号。考虑以下代码:

var x

x

=

200

printf("x = %d", x)

解析器将把三行源代码(第二到第四行)视为一个赋值语句(x = 200 ),并在 200 后面插入一个分号。转换后的代码如下:

var x;

x

=

200;

printf("x = %d", x);

考虑以下打印 20 的代码:

var x = 200, y = 200, z

z = Math.sqrt

(x + y).toString()

print(z)

解析器不会在第三行(z = Math.sqrt)的末尾插入分号。它认为下一行中的(是函数Math.sqrt的参数列表的开始。转换后的代码如下所示:

var x = 200, y = 200, z;

z = Math.sqrt

(x + y).toString();

print(z);

然而,代码的作者可能打算将函数引用Math.sqrt赋给名为z的变量。在这种情况下,分号的自动插入会改变代码的预期含义。如果您自己在赋值语句后插入分号,代码输出会有所不同:

var x = 200, y = 200, z;

z = Math.sqrt;      // Assigns Math.sqrt function reference to z

(x + y).toString(); // Converts x + y to a string and ignores the result

print(z);

function sqrt() { [native code] }

如果您使用关键字void编写了相同的代码来忽略表达式(x + y).toString()的结果,解析器会在文本 z = Math.sqrt后添加一个分号。下面的代码工作起来就好像你打算将函数引用Math.sqrt赋给名为z的变量:

var x = 200, y = 200, z   // A semicolon is inserted here

z = Math.sqrt             // A semicolon is inserted here

void (x + y).toString()   // A semicolon is inserted here

print(z)                  // A semicolon is inserted here

function sqrt() { [native code] }

在第二行末尾插入分号的原因是,关键字void是第三行中一个有问题的标记。关键字void不能是从第二行开始的赋值语句的一部分。

Tip

我建议在任何需要的地方使用分号作为语句结束符。依赖分号的自动插入有时可能会导致细微的错误。

我将在下面几节中简要讨论这些语句类型。

块语句

block 语句的工作方式类似于 Java 中的。它是一组用大括号({ })括起来的零个或多个语句。与 Java 不同,block 语句中声明的变量没有该块的局部范围。可以在声明它们的 block 语句之前和之后访问它们。以下代码片段演示了这一点:

var empId = 100;

// Print empId and deptId. Note that deptId has not been

// declared yet, but you can access it.

print("empId = " + empId + ", deptId = " + deptId);

// A block statement

{

var deptId = 200;

print("empId = " + empId + ", deptId = " + deptId);

// Compute the area of a circle

var radius = 2.3;

var area = Math.PI * radius * radius;

printf("Radius = %.2f, Area = %.2f", radius, area);

}

print("empId = " + empId + ", deptId = " + deptId);

empId = 100, deptId = undefined

empId = 100, deptId = 200

Radius = 2.30, Area = 16.62

empId = 100, deptId = 200

在代码中,变量deptId被声明为块的局部变量。但是,您可以在块的外部(之前和之后)访问它。这不是纳斯雄的 bug。这是根据变量作用域规则设计的。Nashorn 中的变量作用域与 Java 中的很不一样。我将在变量作用域和提升一节中讨论变量的作用域。

可变语句

变量语句用于声明和(可选)初始化变量。我已经在声明变量一节中讨论了变量语句。变量语句的示例如下:

// Declare a variable named empId

var empId;

// Declare a variable named deptId and initialize it to 200

var deptId = 200;

空白语句

分号用作空语句。Nashorn 中的空语句的工作方式与 Java 中的相同。没有效果。它可以用在任何需要语句的地方。像 Java 一样,Nashorn 中的for语句用于迭代目的。下面的代码使用一个for语句来打印整数 1 到 10。空语句用作语句体:

// The semicolon at the end is the empty statement

for(var i = 1; i <= 10; print(i++));

表达式语句

表达式语句是由带/不带副作用的表达式组成的语句。以下是表达式语句的一些示例:

var i = 100; // A variable statement

i++;         // An expression statement

print(i);    // An expression statement

if 语句

Nashorn 中的if语句的工作方式与它在 Java 中的工作方式相同。它的一般语法是:

if(condition)

statement;

对于if语句,也可以有一个else部分:

if(condition)

statement-1;

else

statement-2;

如果condition评估为true,则执行statement-1;否则,执行statement-2。注意,condition可以是任何类型的表达式,不一定是布尔表达式。condition表达式被计算并转换为布尔型值。以下代码片段显示了如何使用一个if和一个if-else语句:

var x = 100, y = 200;

if (x <= y)

printf("%d <= %d", x, y);

// The print() function returns undefined that evaluates to a Boolean false.

if (print(x)) {

print("Inside if");

}

else {

print("Inside else")

}

100 <= 200

100

Inside else

注意,表达式print(x)被用作第二个if语句的conditionprint()函数打印变量x的值并返回undefined。值undefined被转换成布尔型false(请参考布尔型转换一节),它将执行与else部分相关的语句。

迭代语句

Nashorn 支持五种类型的迭代语句:

  • while声明
  • do-while声明
  • for声明
  • for..in声明
  • for..each..in声明

Nashorn 中的whiledo-whilefor语句的工作方式与 Java 中的相同。我不会详细讨论它们,因为作为 Java 开发人员,你知道如何使用它们。下面的代码演示了它们的用法:

// Print first 3 natural numbers using the while,

// do-while, and for statements

var count;

print("Using the while statement...");

count = 1;

while (count <= 3) {

print(count);

count++;

}

print("Using the do-while statement...");

count = 1;

do {

print(count);

count++;

} while (count <= 3);

print("Using the for statement...");

for(var i = 1; i <= 3; i++) {

print(i);

}

Using the while statement...

1

2

3

Using the do-while statement...

1

2

3

Using the for statement...

1

2

3

for..in语句用于迭代数组的索引或对象的属性名。它可以用于数组、列表、地图、任何 Nashorn 对象等集合。它的语法是:

for(var index in object)

Statement;

首先,对object进行评估。如果计算结果为nullundefined,则跳过整个for..in语句。如果它评估为一个对象,该语句将可枚举属性分配给index,并执行主体。该索引属于字符串类型。对于数组,index是数组元素的索引,作为一个字符串。对于任何其他集合,如列表或地图(Nashorn 对象也是地图),对象的属性被分配给index。您可以使用括号符号(object[index])来访问属性的值。下面的代码演示了如何使用for..in语句迭代数组的索引:

// Create an array of three strings

var empNames = ["Ken", "Fred", "Li"];

// Use the for..in statement to iterate over indices of the array

for(var index in empNames) {

var empName = empNames[index];

printf("empNames[%s]=%s", index, empName);

}

empNames[0]=Ken

empNames[1]=Fred

empNames[2]=Li

Tip

index在 for 中...in 语句是字符串类型,而不是数字类型。

for..each..in语句不在 ECMAScript 5.1 规范中。Nashorn 支持的 Mozilla JavaScript 1.6 扩展。当for..in语句遍历集合的索引/属性名时,for..each..in语句遍历集合中的值。它的工作方式与 Java 中的for-each语句相同。请注意,集合是唯一值的集合,没有给这些值命名。不能使用for..in语句迭代集合,但是可以使用for..each..in语句。它的语法是:

for(var value in object)

Statement;

以下代码显示了如何使用for..each..in语句迭代数组的元素(不是索引):

// Create an array of three strings

var empNames = ["Ken", "Fred", "Li"];

// Use the for..each..in statement to iterate over elements of the array

for each(var empName in empNames) {

printf(empName);

}

Ken

Fred

Li

我将在第七章中讨论更多关于for..infor..each..in的语句。

continue、break 和 return 语句

Nashorn 中的continuebreakreturn语句的工作方式与 Java 中的相同。可以对continuebreak语句进行标记。continue语句跳过迭代语句体的其余部分,跳到迭代语句的开头,继续下一次迭代。break语句跳转到它所在的迭代和switch语句的末尾。函数中的return语句将控制权返回给函数的调用者。可选地,return语句也可以向调用者返回值。

with 语句

Nashorn 在一个称为执行上下文的上下文中执行脚本。它使用与执行上下文相关联的作用域链在脚本中查找非限定名。首先在最近的范围内搜索该名称。如果没有找到,则继续沿着范围链向上搜索,直到找到该名称,或者在范围链的顶部执行搜索。它的语法是:

with(expression)

statement

在执行statement时,with语句将计算为对象的指定的expression添加到作用域链的头部。

Tip

不推荐使用with语句,因为它会导致混淆非限定名存在于何处——在with语句指定的对象中,还是在作用域链上的某个位置。在严格模式下是不允许的。

以下代码演示了with语句的用法:

var greetings = new String("Hello");

// Must use greetings.length to access the length property

// of the String object named greetings

printf("greetings = %s, length = %d", greetings, greetings.length);

with(greetings) {

// You can use the length property of the greetings object

// as an unqualified identifier within this with statement.

printf("greetings = %s, length = %d", greetings, length);

}

with(new String("Hi")) {

// The toString() and length will be resolved using the

// new String("Hi") object

printf("greetings = %s, length = %d", toString(), length);

}

with(Math) {

// Compute the area of a circle

var radius = 2.3;

// PI and pow are resolved as properties of the Math object

var area = PI * pow(radius, 2);

printf("Radius = %.2f, Area = %.2f", radius, area);

}

greetings = Hello, length = 5

greetings = Hello, length = 5

greetings = Hi, length = 2

Radius = 2.30, Area = 16. 62

switch 语句

Nashorn 中的switch语句与 Java 中的switch语句非常相似。它的语法是:

switch(expression) {

case expression-1:

statement-1;

case expression-2:

statement-2;

default:

statement-3;

}

使用===操作符将expressioncase子句中的表达式进行匹配。执行第一个匹配的case子句中的语句。如果case子句包含一个break语句,则控制权转移到switch语句的末尾;否则,执行匹配的case子句后的语句。如果没有找到匹配,则执行default子句中的语句。如果有多个匹配,则只执行第一个匹配的case子句中的语句。在 Java 中,expression必须是类型intStringenum,,而在 Nashorn 中,expression可以是任何类型,包括 Object、Null 和 Undefined 类型。以下代码片段演示了如何使用switch语句:

// Define a match function that matches the passed in argument

// using a switch statement

function match(value) {

switch (value) {

case undefined:

print("Matched undefined:", value);

break;

case null:

print("Matched null:", value);

break;

case '2':

print("Matched string '2':", value);

break;

case 2:

print("Matched number 2: ", value);

break;

default:

print("No match:", value);

break;

}

}

// Call the match function with different arguments

match(undefined);

match(null);

match(2);

match('2');

match("Hello");

Matched undefined: undefined

Matched null: null

Matched number 2: 2

Matched string '2': 2

No match: Hello

带标签的陈述

标签只是一个后跟冒号的标识符。Nashorn 中的任何语句都可以通过在语句前放置标签来标记。事实上,Nashorn 允许一个语句有多个标签。一旦你标记了一个语句,你可以使用相同的标签和breakcontinue语句来中断并继续这个标签。通常,您会标记外部迭代语句,以便可以继续或中断嵌套循环。标签语句的工作方式与它们在 Java 中的工作方式相同。以下是使用带标签的语句和带标签的continue语句打印 3x3 矩阵左下半部分的示例:

// Create a 3x3 matrix using an array of arrays

var matrix = [[11, 12, 13],

[21, 22, 23],

[31, 32, 33]];

outerLoop:

for(var i = 0; i < matrix.length; i++) {

for(var j = 0; j < matrix[i].length; j++) {

java.lang.System.out.printf("%d ", matrix[i][j]);

if (i === j) {

print();

continue outerLoop;

}

}

}

11

21 22

31 32 33

throw 语句

一个throw语句用于抛出一个用户定义的异常。它的工作方式类似于 Java 中的throw语句。它的语法是:

throw expression;

在 Java 中,throw语句抛出一个Throwable类或者它的一个子类的对象;expression必须评估为Throwable实例。在 Nashorn 中,throw语句可以抛出任何类型的值,包括数字或字符串。Nashorn 几乎没有内置对象可以用作在throw语句中抛出的错误对象。这样的对象有ErrorTypeErrorRangeErrorSyntaxError等。以下代码片段显示了当empId不在 1 和 10000 之间时如何抛出一个RangeError:

var empId = -900;

if (empId <= 0 || empId >= 10000) {

throw new RangeError(

"empId must be between 1 and 10000\. Found: " + empId);

}

当您运行代码时,它将在标准错误上打印错误的堆栈跟踪。像 Java 一样,Nashorn 允许您处理抛出的错误。您将需要使用一个try-catch-finally语句来处理抛出的错误,这将在下一节中讨论。

try 语句

Nashorn 中的try-catch-finally语句的工作方式与它在 Java 中的工作方式相同。try块包含一个或多个要执行的可能抛出错误的语句。如果语句抛出一个错误,控制被转移到catch块。最后,执行finally块中的语句。像在 Java 中一样,您可以有三种组合的trycatchfinally块:try-catchtry-finallytry-catch-finally。与 Java 不同,ECMAScript 只支持每个try块一个catch块。Nashorn 支持 Mozilla JavaScript 1.4 扩展,允许每个try块有多个catch块。使用try块的语法是:

/* A try-catch block */

try {

// Statements that may throw errors

}

catch(identifier) {

// Handle the error here

}

/* A try-finally block */

try {

// Statements that may throw errors

}

finally {

// Perform cleanup work here

}

/* A try-catch-finally block */

try {

// Statements that may throw errors

}

catch(identifier) {

// Handle the error here

}

finally {

// Perform cleanup work here

}

Tip

在严格模式下,使用evalarguments作为catch块中的标识符是一个SyntaxError

下面是一个带有多个catch块的try块,支持作为 Nashorn 扩展,其中e是一个标识符。您可以使用任何其他标识符来代替e:

/* A try block with multiple catch blocks */

try {

// Statements that may throw errors

}

catch (e if e instanceof RangeError) {

// Handle RangeError here

}

catch (e if e instanceof TypeError) {

// Handle TypeError here

}

catch (e) {

// Handle other errors here

}

考虑清单 4-2 中的代码。它定义了两个函数:isInteger()factorial()。如果isInteger()函数的参数是一个整数,它将返回true;否则,它返回falsefactorial()函数计算并返回自然数的阶乘。如果参数不是数字,它抛出一个TypeError,如果参数不是大于或等于 1 的数字,它抛出一个RangeError

清单 4-2。factorial.js 文件的内容

// factorial.js

// Returns true if n is an integer. Otherwise, returns false.

function isInteger(n) {

return typeof n === "number" && isFinite(n) && n%1 === 0;

}

// Define a function that computes and returns the factorial of an integer

function factorial(n) {

if (!isInteger(n)) {

throw new TypeError(

"The number must be an integer. Found:" + n);

}

if(n < 0) {

throw new RangeError(

"The number must be greater than 0\. Found: " + n);

}

var fact = 1;

for(var counter = n; counter > 1; fact *= counter--);

return fact;

}

清单 4-3 中的程序使用factorial()函数计算一个数字和一个字符串的阶乘。load()函数用于从factorial.js文件加载程序。Error对象(或其子类型)的message属性包含错误消息。程序使用message属性来显示错误信息。当“Hello”作为参数传递给函数时,factorial()函数抛出一个TypeError。程序处理错误并显示错误消息。

清单 4-3。factorial_test.js 文件的内容

// factorial_test.js

// Load the factorial.js file that contains the factorial() function

load("factorial.js");

try {

var fact3 = factorial(3);

print("Factorial of 3 is", fact3);

var factHello = factorial("Hello");

print("Factorial of 3 is", factHello);

}

catch (e if e instanceof RangeError) {

print("A RangeError has occurred.", e.message);

print("Error:", e.message);

}

catch (e if e instanceof TypeError) {

print("A TypeError has occurred.", e.message);

}

catch (e) {

print(e.message);

}

Factorial of 3 is 6

A TypeError has occurred. The number must be an integer. Found:Hello

Nashorn 扩展了 ECMAScript 提供的Error对象。它添加了几个有用的属性来获取抛出的错误的详细信息。表 4-9 列出了这些属性及其描述。

表 4-9。

The List of the Proeprties of Error Object in Nashorn

财产 类型 描述
lineNumber 数字 源代码中引发错误对象的行号
columnNumber 数字 源代码中引发错误对象的列号
fileName 线 源脚本的文件名
stack 线 脚本堆栈以字符串形式跟踪
printStackTrace() 功能 打印完整的堆栈跟踪,包括抛出错误的所有 Java 帧
getStackTrace() 功能 仅为 ECMAScript 框架返回一个java.lang.StackTraceElement实例数组
dumpStack() 功能 像 Java 中的java.lang.Thread.dumpStack()方法一样打印当前线程的堆栈跟踪。dumpStack()Error对象的一个函数属性,你需要调用它作为Error.dumpStack()

清单 4-4 显示了清单 4-3 中程序的另一个版本。这一次,您仅使用catch块,并使用针对Error对象的 Nashorn 扩展来打印错误的详细信息。

清单 4-4。文件 factorial_test2.js 的内容

// factorial_test2.js

load("factorial.js");

try {

// throw new TypeError("A type error occurred.");

var fact3 = factorial(3);

print("Factorial of 3 is", fact3);

var factHello = factorial("Hello");

print("Factorial of 3 is", factHello);

}

catch (e) {

printf("Line %d, column %d, file %s. %s",

e.lineNumber, e.columnNumber, e.fileName, e.message);

}

Factorial of 3 is 6

Line 10, column 8, file factorial.js. The number must be an integer. Found:Hello

调试器语句

debugger语句用于调试目的。它本身不采取任何行动。如果调试器是活动的,实现可能会导致断点,但当它遇到一个debugger语句时,并不要求它这样做。语法是:

debugger;

NetBeans 8.0 IDE 在调试模式下支持debugger语句。我将在第十三章中展示如何使用debugger语句调试 Nashorn 脚本。

定义函数

在 Nashorn 中,函数是一个可执行的参数化代码块,声明一次就可以执行多次。执行函数被称为调用或调用函数。函数是一个对象;它可以有属性,可以作为参数传递给另一个函数;它可以被赋给一个变量。一个函数可以有以下几个部分:

  • 一个名字
  • 形式参数
  • 一具尸体

可以选择给一个函数起一个名字。当一个函数被调用时,你可以传递一些被称为该函数参数的值。实参被复制到函数的形参中。函数体由一系列语句组成。函数可以选择使用return语句返回值。如果没有使用return语句从函数返回,默认情况下,将返回值undefined。Nashorn 中的函数可以用多种方式定义,如后续章节所述。

Tip

你可能认为 Nashorn 中的function是 Java 中的一个方法。但是,请注意,Nashorn 中的函数是一个对象,它有许多不同的用法,而 Java 中的方法是无法使用的。

函数声明

您可以使用function语句声明一个函数,如下所示:

function functionName(param1, param2...) {

function-body

}

关键字function用于声明一个函数。functionName是函数的名称,可以是任何标识符。param1param2等是形式参数名。一个函数可以没有形参,在这种情况下,函数名后面会有一个左括号和一个右括号。由零个或多个语句组成的函数体用大括号括起来。

Tip

在严格模式下,不能使用 eval 和 arguments 作为函数名或函数的形参名。

到目前为止,函数声明中的所有内容看起来都像是 Java 中的方法声明。但是,有两个显著的区别:

  • 函数声明没有返回类型
  • 函数只指定形参的名称,而不指定它们的类型

下面是一个名为adder的函数的例子,它接受两个参数,并通过对它们应用+运算符来返回值:

// Applies the + operator on parameters named x and y,

// and returns the result

function adder(x, y) {

return x + y;

}

在 Java 中调用函数和调用方法是一样的。您需要使用函数名,后跟用圆括号括起来的逗号分隔的参数列表。以下代码片段调用带有两个值分别为 5 和 10 的参数的adder函数,并将该函数的返回值赋给一个名为sum的变量:

var sum = adder(5, 10); // Assigns 15 to sum

考虑以下代码片段和输出:

var sum1 = adder(5, true); // Assigns 6 to sum1

var sum2 = adder(5, "10"); // Assigns "510" to sum2

print(sum1, typeof sum1);

print(sum2, typeof sum2);

6 number

510 string

这会给你一个惊喜。您可能已经编写了adder()函数,记住要将两个数相加。但是,您能够传递 Number、Boolean 和 String 类型的参数。事实上,您可以向该函数传递任何类型的参数。这是因为 Nashorn 是一种松散类型的语言,类型检查是在运行时执行的。在给定的上下文中,所有值都被转换为预期的类型。在第一个调用adder(5, true)中,布尔值true被自动转换为数字 1;在第二个调用adder(5, "10")中,数字 5 被转换为字符串“5 ”,+操作符作为字符串连接操作符。如果你希望函数中的参数是类型安全的,你需要自己验证它们,就像你在清单 4-2 的factorial()函数中所做的那样。

Tip

每个函数都有一个只读属性,名为length,它包含函数的形参数量。在我们的例子中,adder.length将返回 2,因为adder()函数声明了两个名为xy的形参。

函数是一个对象。函数名是对函数对象的引用。您可以将函数名赋给另一个变量,并将其传递给其他函数。以下代码片段将对adder()函数的引用分配给一个名为myAdder的变量,并使用myAdder变量调用该函数:

// Assigns the reference of the adder function to the variable myAdder

var myAdder = adder;

// Call the function (adder()) referenced by myAdder

var sum = myAdder(5, 10);

print(sum);

15

使用函数参数

在我讨论函数中的参数传递是如何工作的之前,考虑下面的代码片段,它用零到三个参数调用adder()函数:

var sum1 = adder();         // Passes no arguments

var sum2 = adder(10);       // Passes only one arguments

var sum3 = adder(10, 5);    // Passes two arguments

var sum4 = adder(10, 5, 9); // Passes three arguments - one extra

print("sum1 = " + sum1)

print("sum2 = " + sum2)

print("sum3 = " + sum3)

print("sum4 = " + sum4)

sum1 = NaN

sum2 = NaN

sum3 = 15

sum4 = 15

首先要注意的是,代码执行时没有任何错误,也就是说,Nashorn 允许您向函数传递少于或多于函数形参数量的参数。这个特性是好是坏取决于你如何看待它。从某种意义上说,您可以认为 Nashorn 中的所有函数都是 varargs,其中可以命名一些形参。从坏的方面来说,您可能会认为,如果您希望调用方传递的参数数量与声明的形参数量相同,就需要验证传递给函数的参数数量。

在每个函数体内,都有一个名为arguments的对象引用。它的行为就像一个数组,但它是一个对象,而不是一个数组。它的length属性是传递给函数的实际参数的数量。实际的参数使用索引 0、1、2、3 等存储在arguments对象中。第一个参数是arguments[0],第二个参数是arguments[1],以此类推。以下规则适用于函数调用的参数:

  • 传递给函数的参数存储在arguments对象中
  • 如果传递的参数数量小于声明的形参,则未填充的形参被初始化为undefined
  • 如果传递的参数的数量大于形参的数量,您可以使用arguments对象来访问额外的参数。事实上,您可以使用arguments对象随时访问所有参数
  • 如果为形参传递一个变量,形参名称和该形参的arguments对象索引属性被绑定到同一个值。改变参数值或arguments对象中的相应值会改变两者

清单 4-5 显示了一个avg()函数的代码,它计算传递参数的平均值。该函数不声明任何形参。它检查是否至少传递了两个参数,并且所有参数都必须是数字(原始数字或数字对象)。

清单 4-5。使用 arguments 对象访问所有传递的参数的函数

// avg.js

function avg() {

// Make sure at least two arguments are passed

If (arguments.length < 2) {

throw new Error(

"Minimum 2 arguments are required to compute average.");

}

// Compute the sum of all arguments

var sum = 0;

for each (var arg in arguments) {

if (!(typeof arg === "number" ||

arg instanceof Number)) {

throw new Error("Not a number: " + arg);

}

sum += arg;

}

// Compute and return the average

return sum / arguments.length;

}

下面的代码片段调用了avg()函数:

// Load avg.js file, so the avg() function is available

load("avg.js");

printf("avg(1, 2, 3) = %.2f", avg(1, 2, 3));

printf("avg(12, 15, 300, 8) = %.2f", avg(12, 15, 300, 8));

avg(1, 2, 3) = 2.00

avg(12, 15, 300, 8) = 83.75

函数表达式

函数表达式是可以在任何可以定义表达式的地方定义的函数。函数表达式看起来非常类似于函数声明,除了函数名是可选的。下面是一个函数表达式示例,它将函数定义为赋值表达式的一部分:

var fact = function factorial(n) {

if (n <= 1) {

return 1;

}

var f = 1;

for(var i = n; i > 1; f *= i--);

return f;

};

/* Here, you can use the variable name fact to call the function,

not the function name factorial.

*/

var f1 = fact(3);

var f2 = fact(7);

printf("fact(3) = %d", f1);

printf("fact(10) = %d", f2);

fact(3) = 6

fact(10) = 5040

请注意函数表达式中右大括号末尾的分号,它是赋值语句的终止符。您已经为函数表达式命名为factorial。然而,除了在函数体内部之外,名字factorial不能用作函数名。如果要调用函数,必须使用存储了函数引用的变量。下面的代码显示了函数体中函数表达式的函数名的用法。这段代码使用递归函数调用来计算阶乘:

var fact = function``factorial

if (n <= 1) {

return 1;

}

// Uses the function name factorial to call itself

return n *``factorial

};

var f1 = fact(3);

var f2 = fact(7);

printf("fact(3) = %d", f1);

printf("fact(10) = %d", f2);

fact(3) = 6

fact(10) = 5040

函数表达式中的函数名是可选的。代码可以写成如下形式:

// There is no function name in the function expression. It is an // anonymous function.

var fact = function (n) {

if (n <= 1) {

return 1;

}

var f = 1;

for(var i = n; i > 1; f *= i--);

return f;

};

var f1 = fact(3);

var f2 = fact(7);

printf("fact(3) = %d", f1);

printf("fact(10) = %d", f2);

fact(3) = 6

fact(10) = 5040

还可以定义一个函数表达式,并在同一个表达式中调用它。以下代码显示了如何以两种不同的方式定义和调用函数表达式:

// Load the avg.js file, so the avg() function will be available

load("avg.js");

// Encloses the entire expression in (). Defines a function expression and calls

// it at the same time.

(function printAvg(n1, n2, n3){

// Call the avg() function

var average = avg(n1, n2, n3);

printf("Avarage of %.2f, %.2f and %.2f is %.2f.", n1, n2, n3, average);

}(10, 20, 40));

// Uses the void operator to create an expression. Defines a function // expression and calls it at the same time.

void function printAvg(n1, n2, n3) {

var average = avg(n1, n2, n3);

printf("Avarage of %.2f, %.2f and %.2f is %.2f.", n1, n2, n3, average);

}(10, 20, 40);

Avarage of 10.00, 20.00 and 40.00 is 23.33.

Avarage of 10.00, 20.00 and 40.00 is 23.33.

首先,加载定义了avg()函数的avg.js文件。函数表达式需要用圆括号括起来,或者在前面加上void操作符,以帮助解析器不要将其混淆为函数声明。函数参数列表跟在函数表达式右大括号后面。在第二种情况下,您使用了void操作符而不是括号。解析器将能够正确地解析函数参数,因为它期望在void操作符之后有一个表达式,而不是函数声明。您可以在两个函数表达式的代码中删除函数名。

Nashorn 支持 Mozilla JavaScript 1.8 扩展,这是定义函数表达式的简写,函数表达式的主体只由一个表达式组成。在这种情况下,可以从函数体中去掉大括号和return语句。以下代码使用简写语法定义了adder函数表达式:

// The function expressions' body does not use {} and a return statement

var adder = function(x, y) x + y;

// Call the function using the adder variable

printf("adder(10, 5) = %d", adder(10, 5));

adder(10, 5) = 15

作为 Java 开发人员,你可以将函数表达式与 lambda 表达式和匿名类进行比较。通常,函数表达式被用作回调,并封装不应该向全局范围公开的业务逻辑。

Function()构造函数

您也可以使用Function构造函数或Function函数创建一个函数对象。它允许你从一个字符串创建一个函数对象。Nashorn 中的构造函数是一个使用new操作符创建新对象的函数。Nashorn 有一个内置函数,名为Function,,可以用作构造函数。使用Function构造函数创建函数的语法是:

var func = new Function("param1", "param2"..., "function-body");

也可以简单的用Function作为函数,如下图:

var func = Function("param1", "param2"..., "function-body");

param1param2等是正在定义的新函数的形参的名称。函数体是函数的体。Function的所有参数都作为字符串传递。如果你只传递一个参数,它被认为是函数的主体。您也可以在一个字符串中指定所有参数名称,方法是用逗号分隔名称,如下所示:

var func = Function("param1, param2,...", "function-body");

下面的代码创建了我们之前创建的adder()函数。这一次,我们使用了Function对象:

// Create a function that takes two arguments and returns

// the value after applying the + operator

var adder = new Function("x", "y", "return x + y;")

printf("adder(10, 15) = %d", adder(10, 15));

printf("adder('Hello', ' world') = %s", adder('Hello', ' world'));

adder(10, 15) = 25

adder('Hello', ' world') = Hello world

有时,您可能需要一个不带参数、不执行任何逻辑的空函数。一个空函数在被调用时总是返回undefined。您可以通过不向Function指定任何参数来创建这样一个空函数,如下所示:

// Define an empty function

var emptyFunction = Function();

// Print the string form of the new function

print(emptyFunction);

// Call the function that will return undefined

var nothing = emptyFunction();

print(nothing);

function () {

}

undefined

建议您不要使用Function对象来定义函数,因为运行时无法对字符串中包含的函数体进行优化,并且每次遇到使用Function的表达式时都会创建函数。

对象类型

Nashorn 中的对象是属性的集合。有两种类型的属性:

  • 命名数据属性
  • 命名访问器属性

命名数据属性将名称与值相关联。该值可以是原始值、对象或函数。您可以将 Nashorn 中对象的命名数据属性视为 Java 对象的实例变量或方法。

命名访问器属性将名称与一个或两个访问器函数相关联。这些函数也被称为 getter 和 setter。访问器函数用于获取或设置值。当使用命名的访问器属性(赋值或读取)时,会调用相应的访问器函数。您可以将命名的访问器属性视为 Java 对象的 getter/setter 方法。

您还可以为对象的属性指定一些布尔属性。例如,您可以将对象的writable属性设置为false,使该属性为只读。在 Nashorn 中创建对象有几种方法:

  • 使用对象文字
  • 使用构造函数
  • 使用Object.create()方法

下一节将解释如何使用这些方法创建对象。

使用对象文字

对象文字是创建和初始化对象的表达式。使用对象文字的语法是:

{propName1:value1, propName2:value2, propName3:value3,...}

对象文字用大括号括起来。每个属性由一个名称-值对组成。属性的名称和值由冒号分隔。两个属性由逗号分隔。最后一个属性值后允许有尾随逗号。在对象文字中,propName1propName2等是属性的名称,value1value2等是它们的值。属性的名称可以是标识符、用单引号或双引号括起来的字符串,或者只是一个数字。如果属性名包含空格,则必须用单引号或双引号引起来。您也可以使用空字符串作为对象的属性名。

以这种方式定义的属性称为对象的自身属性。注意,一个对象可以从它的原型继承属性,这些属性被称为继承属性。以下语句使用对象文本创建几个对象:

// An object with no own properties

var emptyObject = {};

// An object with two own properties named x and y

var origin2D = {x:0, y:0};

// An object with three own properties named x, y, and z

var origin3D = {x:0, y:0, z:0};

// An object with whitespaces in property names

var redColor = {"red value": 1.0,

green: 0.0,

"black value": 0.0,

alpha: 1.0};

访问对象的属性

您可以使用两种语法之一,通过属性访问器表达式来访问对象的属性:

  • 使用点符号
  • 使用括号符号

点符号使用以下语法:

objectExpression.property

objectExpression是一个表达式,其计算结果是对对象的引用,而property是属性名。

括号符号使用类似数组的语法:

objectExpression[propertyExpression]

objectExpression是一个表达式,其计算结果是对对象的引用。接下来是左括号和右括号。propertyExpression是一个可转换为字符串的表达式,它是被访问的属性名。

Tip

如果属性名包含空格或存储在变量中,则必须使用括号符号而不是点符号来访问属性。

考虑上一节中的以下对象:

// An object with whitespaces in the property names

var redColor = {"red value": 1.0,

green: 0.0,

"black Value": 0.0,

alpha: 1.0};

以下语句将对象的alpha属性的值读入名为alphaValue的变量中:

// Assigns 1.0 to alphaValue

var alphaValue = redColor.alpha;

如果属性访问器表达式出现在赋值运算符的右侧,则表示您正在为属性设置新值。以下语句将redColor对象的alpha属性设置为 0.5:

// Make the color semi-transparent

redColor.alpha = 0.5;

以下代码片段使用括号符号执行相同的操作:

// Assigns 1.0 to alphaValue

var alphaValue = redColor["alpha"];

// Make the color semi-transparent

redColor["alpha"] = 0.5;

"red value""black value"属性包含一个空格,所以您只能使用括号符号来访问它们:

// Assigns 1.0 to redValue

var redValue = redColor["red value"];

// Assigns 0.8 to the "red value" property

redColor["red value"] = 0.8

使用括号表示法时,可以使用任何可以转换为字符串形式的属性名的表达式。以下代码片段以两种方式将值 0.8 分配给"red Value"属性:

var prop = "red value";

redColor[prop] = 0.8;                   // Using a variable

redColor["red" + " " + "value"] = 0.8;  // Using an expression

考虑下面的代码,它用两个名为xy的属性定义了一个point2D对象,并试图读取一个名为z的不存在的属性:

// Define a point2D object

var point2D = {x:10, y:-20};

// Try accessing x, y, and z properties

var x = point2D.x;

var y = point2D.y;

var z = point2D.z;

print("x = " + x + ", y =" + y + ", z = " + z);

x = 10, y =-20, z = undefined

你对输出感到惊讶吗?在 Nashorn 中访问不存在的对象属性不会产生错误。如果读取一个不存在的属性,则返回undefined;如果设置了不存在的属性,则会使用新值创建一个同名的新属性。以下代码显示了这种行为:

// Create an object with one property x

var point3D = {x:10};

// Create a new property named y and assign -20 to it

point3D.y = -20;

// Create a new property named z and assign 35 to it

point3D.z = 35;

// Print all properties of point3D

print("x = " + point3D.x + ", y =" + point3D.y + ", z = " + point3D.z);

x = 10, y =-20, z = 35

如何知道不存在的属性和值为undefined的现有属性之间的区别?您可以使用in操作符来知道一个属性是否存在于一个对象中。语法是:

propertyNameExpression in objectExpression

propertyNameExpression评估为一个字符串,它是属性的名称。objectExpression评估为一个对象。如果对象具有指定名称的属性,in操作符返回true;否则,它返回falsein操作符搜索对象的自身属性和继承属性。以下代码显示了如何使用in操作符:

// Create an object with x and y properties

var colorPoint2D = {x:10, y:20};

// Check if the object has a property named x

var xExists = "x" in colorPoint2D;

print("Property x exists: " + xExists + ", x = " + colorPoint2D.x);

// Check if the object has a property named color

var colorExists = "color" in colorPoint2D;

print("Property color exists: " + colorExists + ", color = " + colorPoint2D.color);

// Add a color property and set it to undefined, and then, perform the check

colorPoint2D.color = undefined;

colorExists = "color" in colorPoint2D;

print("Property color exists: " + colorExists + ", color = " + colorPoint2D.color);

Property x exists: true, x = 10

Property color exists: false, color = undefined

Property color exists: true, color = undefined

下面的语句创建一个具有三个属性的对象;其中两个包含数据值,另一个包含函数。它向您展示了如何使用作为函数的属性。您需要调用传递参数的函数,如果有的话:

// A person object with fName, lName, and getFullName properties

var john = {fName: "John",

lName: "Jacobs",

getFullName: function () {

return this.fName + " " + this.lName;

}

};

var fullName = john.getFullName();

print("Full name is " + fullName);

Full name is John Jacobs

注意函数中关键字this的使用。关键字this指的是调用函数的对象。

Tip

如果一个对象的数据属性有一个函数作为它的值,这样的函数被称为该对象的一个方法。换句话说,方法是定义为对象属性的函数。

您也可以使用括号符号访问作为函数的属性。语法看起来有点别扭。使用点符号的函数调用可以替换为括号符号,如下所示:

var fullName = john["getFullName"]();

print("Full name is " + fullName);

Full name is John Jacobs

定义访问者属性

访问器属性也称为 getter 和 setter 方法。您可以将访问器属性看作 Java 类中的一组getXxx()setXxx()方法。当读取xxx属性时,调用名为xxx的 getter 方法;当设置了xxx属性时,名为xxx的 setter 方法被调用。getter 方法没有声明形参。setter 方法声明了一个形参。一个属性只能有一个 getter 方法和/或 setter 方法。以下是定义访问者属性的语法:

{ prop1: value1, /* A data property */

prop2: value2, /* A data property */

/* The getter for the property propName */

get propName() {

// Getter method's body goes here

},

/* The setter for the property propName */

set propName(propValue) {

// Setter method's body goes here

}

}

定义一个访问器属性等同于用关键字getset替换关键字function来声明函数。关键字getset分别用于定义 getter 和 setter 方法。请注意,您没有使用冒号来定义访问者属性,但是您仍然需要使用逗号来分隔对象的两个属性。以下代码定义了一个对象,该对象具有两个名为fNamelName的数据属性,以及一个名为fullName的访问器属性:

// A person object with fName and lName as data proeprties

// and fullName as an accessor property

var john = {fName: "John",

lName: "Jacobs",

get fullName() {

return this.fName + " " + this.lName;

},

set fullName(name) {

names = name.split(" ");

if(names.length === 2) {

this.fName = names[0];

this.lName = names[1];

}

else {

throw new Error("Full name must be in the form 'fName lName'.");

}

}

};

// Get the full name using the fullName accessor property and print it

print("Full name is " + john.fullName);

// Set a new full name

john.fullName = "Ken McEwen";

// Get the new full name and print it

print("New full name is " + john.fullName);

Full name is John Jacobs

New full name is Ken McEwen

注意,您通过设置fullName属性来设置这个人的名字和姓氏。当设置fullName属性的值时,如果该值遵循"fName lName"格式,则该值被传递给设置名字和姓氏的属性的 setter 方法。

设置属性特性

您可以设置数据的属性和对象的访问者属性。表 4-10 列出了属性的属性。请注意,并非所有属性都适用于所有类型的属性。

表 4-10。

The List of Property Attributes and Their Descriptions

属性 类型 适用于 描述
value 任何类型 仅数据属性 财产的价值
writable 布尔代数学体系的 仅数据属性 指定属性的值是否可以更改。如果false,该属性是只读的。否则,该属性是可读写的。默认为true
get 功能 仅访问器属性 属性或undefined的 getter 方法。默认为undefined
set 功能 仅访问器属性 undefined属性的 setter 方法。默认为undefined
enumerable 布尔代数学体系的 两者 如果设置为false,则不能使用for..infor..each..in循环枚举对象的属性。默认为true
configurable 布尔代数学体系的 两者 设置为false,属性不能被删除,属性的属性不能被改变。默认为true

您可以使用名为属性描述符的对象来读取和设置属性。下面的语句创建一个属性描述符,该描述符将value属性设为 10,将writable属性设为false:

var descriptor = {value:10, writable:false};

创建属性描述符没有任何作用。它只是创建了一个对象。您需要使用Object的以下三种方法之一来定义一个带有属性的新属性,改变一个已经定义的属性的属性,以及读取一个已有属性的属性描述符:

  • Object.defineProperty(object, "propertyName", propertyDescriptor)
  • Object.defineProperty(object, "propertyName", attributeObject)
  • Object.getOwnPropertyDescriptor(object, "propertyName")

defineProperty()功能允许您定义一个带有属性的新属性或更改现有属性的属性。使用defineProperties()函数,您可以做同样的事情,但是需要多个属性。getOwnPropertyDescriptor()函数返回对象指定属性的描述符。所有这三个函数都与对象的自身属性一起工作。

以下代码定义了一个origin2D对象来定义 2D 坐标系中的原点,并使xy属性不可写:

// Define an object

var origin2D = {x:0, y:0};

// Read the property descriptors for x and y

var xDesc = Object.getOwnPropertyDescriptor(origin2D, "x");

var yDesc = Object.getOwnPropertyDescriptor(origin2D, "y");

printf("x.value = %d, x.writable = %b", xDesc.value, xDesc.writable);

printf("y.value = %d, y.writable = %b", yDesc.value, yDesc.writable);

// Make x and y non-writable

Object.defineProperty(origin2D, "x", {writable:false});

Object.defineProperty(origin2D, "y", {writable:false});

print("After setting x and y non-writable... ")

// Read the property descriptors for x and y again

var xDesc = Object.getOwnPropertyDescriptor(origin2D, "x");

var yDesc = Object.getOwnPropertyDescriptor(origin2D, "y");

printf("x.value = %d, x.writable = %b", xDesc.value, xDesc.writable);

printf("y.value = %d, y.writable = %b", yDesc.value, yDesc.writable);

x.value = 0, x.writable = true

y.value = 0, y.writable = true

After setting x and y non-writable...

x.value = 0, x.writable = false

y.value = 0, y.writable = false

以下代码向您展示了如何使用Object.defineProperty()函数添加新属性:

// Define an object with no properties

var origin2D = {};

// Add two non-writable x and y properties to origina2D with their value set to 0.

Object.defineProperty(origin2D, "x", {value:0, writable:false});

Object.defineProperty(origin2D, "y", {value:0, writable:false});

在这段代码中,您可以使用defineProperties()函数一次性定义xy属性。函数defineProperty()defineProperties()都返回正在设置属性的对象,这样你就可以链接它们的调用。以下代码重写了前面的示例:

// Create an empty object

var origin2D = {};

// Add two non-writable x and y properties to origina2D

// with their value set to 0.

Object.defineProperties(origin2D, {x: {value:0, writable: false},

y: {value:0, writable: false}});

Tip

当您在 object literal 中或通过给属性赋值来给对象添加属性时,writableenumerableconfigurable属性的默认值被设置为true。当您使用属性描述符定义一个属性或者改变属性的属性时,属性描述符中这些属性的缺省值是false。确保在使用属性描述符时为所有需要设置为true的属性指定值。

一旦您将属性的writable属性设置为false,更改其值将不起作用。在严格模式下,更改不可写属性的值会生成错误:

var point = {x:0, y:10};

printf("x = %d", point.x);

// Make x non-writable

Object.defineProperty(point, "x", {writable: false});

// Try changing the value of x

point.x = 100; // Has no effect, because x is non-writable

printf("x = %d", point.x);

x = 0

x = 0

在 Nashorn 中定义常数并不简单。您将需要使用Object.defineProperty()函数来定义一个常数。下面的代码在全局范围内定义了一个名为MAX_SIZE的常量。关键字this指的是下面代码中的全局对象:

// Define a constant named MAX_SIZE with a value 100

Object.defineProperty(this, "MAX_SIZE", {value:100, writable:false, configurable:false});

printf("MAX_SIZE = %d", MAX_SIZE);

MAX_SIZE = 100

您可以使用for..infor..each..in迭代器语句迭代对象的enumerable属性。下面的代码创建一个具有两个默认属性enumerable的对象,使用for..infor..each..in语句迭代属性,将其中一个属性更改为不可数,然后再次迭代属性:

// Create an object with two properties x and y

var point = {x:10, y:20};

// Using for..in reports x and y as properties

for(var prop in point) {

printf("point[%s] = %d", prop, point[prop]);

}

// Make x non-enumerable

Object.defineProperty(point, "x", {enumerable: false});

print("After making x non-enumerable");

// Using for..in reports only y as property

// because x is now non-enumerable

for(var prop in point) {

printf("point[%s] = %d", prop, point[prop]);

}

point[x] = 10

point[y] = 20

After making x non-enumerable

point[y] = 20

删除对象的属性

您可以使用delete操作符删除对象的configurable属性。它的语法是:

delete property;

下面的代码片段创建一个具有两个属性xy的对象,遍历属性,删除名为x的属性,并再次遍历属性。第二次,属性x及其值没有被打印出来,因为它已经被删除了:

// Create an object with two properties x and y

var point = {x:10, y:20};

for(var prop in point) {

printf("point[%s] = %d", prop, point[prop]);

}

// Delete property x from the point object

delete point.x;

print("After deleting x");

for(var prop in point) {

printf("point[%s] = %d", prop, point[prop]);

}

point[x] = 10

point[y] = 20

After deleting x

point[y] = 20

在严格模式下,删除属性configurable设置为false的属性是错误的。在非严格模式下删除不可配置的属性没有任何效果。

使用构造函数

构造函数(或简称为构造函数)是与new操作符一起使用来创建对象的函数。习惯上,构造函数以大写字母开头。下面的代码创建了一个名为Person的函数,旨在用作构造函数:

// Declare a constructor named Person

function Person(fName, lName) {

this.fName = fName;

this.lName = lName;

this.fullName = function () {

return this.fName + " " + this.lName;

}

}

注意,构造函数只是一个像其他函数一样的函数,它也可以在不使用new关键字的情况下作为函数被调用。结果可能会非常不同,这取决于函数是如何编写的。我将很快讨论这样的场景。

构造函数中的关键字this指的是用new操作符调用函数时被构造的对象。在这种情况下,Person函数中的this.fName指的是正在构造的新对象的fName属性。lNametoString也是。Person构造函数简单地给正在创建的对象添加了三个属性。以下代码片段使用Person构造函数创建了两个对象,并打印了它们的字符串表示:

// Create few Person objects

var john = new Person("John", "Jacobs");

var ken = new Person("Ken", "McEwen");

// The print() function calls the toString() method when

// it needs to convertan object to a string

print(john);

print(ken);

John Jacobs

Ken McEwen

让我们试着简单地将Person用作函数,而不是构造函数:

// Print details

printf("fName = %s, lName = %s", this.fName, this.lName);

// Call the Person function

var john = Person("John", "Jacobs");

// Print details

printf("fName = %s, lName = %s, full name = %s", this.fName, this.lName, this.fullName());

// Call the Person function

var ken = Person("Ken", "McEwen");

// The print two person references

print("john = " + john);

print("ken = " + ken);

printf("fName = %s, lName = %s, full name = %s", this.fName, this.lName, this.fullName());

fName = undefined, lName = undefined

fName = John, lName = Jacobs, full name = John Jacobs

john = undefined

ken = undefined

fName = Ken, lName = McEwen, full name = Ken McEwen

您可以在代码中观察到以下几点:

  • 代码打印出this.fNamethis.lName的值。在全局上下文中调用printf()函数,关键字this指的是全局对象。因为您没有为全局对象定义任何fNamelName属性,所以这两个属性的值都作为undefined返回。
  • 调用Person()函数,其返回值存储在john变量中。Person()函数是在全局上下文中调用的,所以函数内部的关键字this指的是全局对象。该函数向全局对象添加三个属性。
  • printf()函数用于打印全局对象的详细信息(由this引用),以确认之前对Person()函数的调用将这些属性添加到了全局对象中。
  • 用不同的参数再次调用Person()函数。第一次调用已经向全局对象添加了三个属性。这个调用只是用新值更新它们。通过再次从全局对象中读取这些属性来确认这一点。
  • 因为Person()函数没有显式返回值,所以它默认返回undefined,这由输出中的最后两行确认。

如果不向构造函数传递任何参数,可以省略新对象创建表达式中的括号。下面的代码调用Person作为构造函数,而不传递任何参数,这些参数将函数内部的实际参数默认为undefined。注意,new操作符后面简单地跟了构造函数名:

// Create a person with both names as undefined

var person = new Person;  // No arguments list

print(person.fullName());

// Set the names for the person

person.fName = "Ken";

person.lName = "Smith";

print(person.fullName());

undefined undefined

Ken Smith

通常情况下,构造函数不会在它的主体中使用return语句。如果它不返回值或原始值,则使用新对象,忽略原始值。如果它返回一个对象,则返回的对象被用作调用new操作符的值。构造函数的返回值可能会改变new操作符的返回对象,这听起来可能有点奇怪;然而,这是该语言的一个强大功能。您可以使用它来实现一个函数,该函数既可以用作函数,也可以用作构造函数,并且都返回一个对象。您可以使用它来缓存对象,并在缓存中已经存在新对象时返回缓存的对象。清单 4-6 展示了这样一种技术。

清单 4-6。Logger.js 文件的内容

// Logger.js

// Declare a function object named Logger

function Logger() {

// A private method

function getLogger() {

if (!Logger.logger) {

// Create a new logger and store its reference in

// the looger pproperty of the Logger function

Logger.logger = {log: function(msg) {

print(msg);

}

};

}

return Logger.logger;

}

return getLogger();

}

// Create two logger objects

var logger1 = new Logger(); // A constructor call

var logger2 = new Logger(); // A constructor call

var logger3 = Logger();     // A function call

// Check if the logger is cached

print("logger1 === logger2 is " + (logger1 === logger2));

print("logger1 === logger3 is " + (logger1 === logger3));

logger1.log("Hello 1");

logger2.log("Hello 2");

logger3.log("Hello 3");

logger1 === logger2 is true

logger1 === logger3 is true

Hello 1

Hello 2

Hello 3

第一次调用Logger时,作为一个函数或者构造函数,用log()方法创建一个对象,在Logger函数的logger属性中缓存该对象,并返回该对象。当再次调用Logger时,它只是返回缓存的对象。

使用构造函数,可以维护对象的私有状态。这是使用闭包来完成的。如果在函数中用关键字var定义一个变量,该变量具有局部范围。它只能在函数内部访问。嵌套函数捕获其封闭范围,包括在其外部函数中声明的局部变量。清单 4-7 展示了使用构造函数维护对象私有状态的概念。它创建了一个维护当前值的Sequence对象,该对象只能通过方法curValue()nextValue()访问。注意,当对象被创建时,局部变量currentValue被两个函数捕获。调用这些函数时,它们处理相同的捕获变量,如输出所示。

清单 4-7。Sequence.js 文件的内容

// Sequence.js

// This object generates strictly increasing sequence numbers

function Sequence() {

var currentValue = 0;

// Using Nashorn extension syntax to define one-liner functions

this.nextValue = function () ++currentValue;

this.curValue = function () currentValue;

}

// Create a Sequence object

var empId = new Sequence();

print("empId sequence...");

printf("Current Value = %d, next Value = %d", empId.curValue(), empId.nextValue());

printf("Current Value = %d, next Value = %d", empId.curValue(), empId.nextValue());

printf("Current Value = %d, next Value = %d", empId.curValue(), empId.nextValue());

// Create a Sequence object

var deptId = new Sequence();

print("deptId sequence...");

printf("Current Value = %d, next Value = %d", deptId.curValue(), deptId.nextValue());

printf("Current Value = %d, next Value = %d", deptId.curValue(), deptId.nextValue());

printf("Current Value = %d, next Value = %d", deptId.curValue(), deptId.nextValue());

empId sequence...

Current Value = 0, next Value = 1

Current Value = 1, next Value = 2

Current Value = 2, next Value = 3

deptId sequence...

Current Value = 0, next Value = 1

Current Value = 1, next Value = 2

Current Value = 2, next Value = 3

对象继承

与 Java 不同,Nashorn 没有类。Nashorn 只处理对象。它支持基于原型的对象继承。除了属性集合,Nashorn 中的每个对象都有一个原型,即一个对象或null值。您可以使用__proto__属性访问对象原型的引用。注意,在属性名__proto__中,单词proto的前后有两个下划线。此属性已被否决,您可以使用以下两个函数来获取和设置对象的原型:

  • Object.getPrototypeOf(object)
  • Object.setPrototypeOf(object, newPrototype)

如果访问对象中的属性,首先搜索它自己的属性。如果找到属性,则返回该属性。如果找不到该属性,则搜索该对象的原型。如果找不到该属性,则搜索该对象的原型的原型,依此类推。搜索将继续,直到在原型链中找到该属性或搜索到整个原型链。这种搜索与 Java 中基于类的继承是一样的,其中属性沿着类继承链向上搜索,直到搜索到java.lang.Object类。

Tip

在 Java 中,类继承总是扩展到java.lang.Object类。在 Nashorn 中,Object.prototype是对象的默认原型。但是,您可以将原型设置为任何对象,包括null值。

考虑清单 4-8 中的代码。

清单 4-8。prototype.js 文件的内容

// prototype.js

var point = {x: 10,

y: 20,

print: function() {

printf("(%d, %d)", this.x, this.y);

}

};

var coloredPoint = {color: "red",

print: function() {

printf("(%d, %d, %s)", this.x, this.y,                                          this.color);

}

};

// Set the point object as the prototype of the coloredPoint object

// That is, the coloredPoint object inherits from the point object.

Object.setPrototypeOf(coloredPoint, point);

print("After setting the prototype for coloredPoint...");

// Call the print() methods of both objects

point.print();

coloredPoint.print();

// Change the x and y values in the point object.

point.x = 100;

point.y = 200;

print("After setting the x and y properties for point...");

// Print the two points details again

point.print();

coloredPoint.print();

/* Call the toString() method that is defined in Object.prototype object and are

available in point and coloredPoint object through prototype chain.

*/

print(point.toString());

print(coloredPoint.toString());

// Print prototypes of objects

print("Object.getPrototypeOf(point) === Object.prototype is " +

(Object.getPrototypeOf(point) === Object.prototype));

print("Object.getPrototypeOf(colorPoint) === point is " +

(Object.getPrototypeOf(coloredPoint) === point));

print("Object.getPrototypeOf(colorPoint) === Object.prototype is " +

(Object.getPrototypeOf(coloredPoint) === Object.prototype));

After setting the prototype for coloredPoint...

(10, 20)

(10, 20, red)

After setting the x and y properties for point...

(100, 200)

(100, 200, red)

[object Object]

[object Object]

Object.getPrototypeOf(point) === Object.prototype is true

Object.getPrototypeOf(colorPoint) === point is true

Object.getPrototypeOf(colorPoint) === Object.prototype is false

您可以在代码中观察到以下几点:

  • 它定义了一个具有三个属性的point对象,这三个属性分别是xyprint
  • 它定义了另一个名为coloredPoint ,的对象,具有两个名为colorprint的属性。此时,这两个对象是不相关的。
  • 它使用Object.setPrototypeOf()函数将point对象设置为coloredPoint对象的原型,所以现在coloredPoint对象继承了point对象。此时,coloredPoint对象继承了point对象的所有属性,包括属性xy。注意,x 和y只有一个副本,它们由pointcoloredPoint对象共享。
  • 它在两个对象上调用print()方法。输出确认两个对象都调用了各自版本的print()方法。这是一个财产超越的案例。coloredPoint对象覆盖了point对象的print()方法。这与 Java 中的工作原理相同。
  • 它更改了point对象上的xy属性的值,并再次调用这两个对象的print()方法。输出确认了pointcoloredPoint对象的 x 和 y 值都已更改。
  • 它对两个对象调用toString()方法,它们打印相同的字符串。这是另一个继承的例子。point对象没有设置它的原型,所以默认情况下,它的原型被设置为Object.prototype对象,其中Object是 Nashorn 中的内置对象。toString()方法在Object.prototype中定义,默认情况下被所有对象继承。coloredPoint对象从point对象继承而来,point对象从Object.prototype对象继承而来。
  • 最后,它比较对象的原型并打印结果。输出证实了所讨论的原型链。

图 4-1 描绘了pointcoloredPoint对象的原型链。注意,Object.prototype将其原型设置为null,表示原型链的结束。您可以将point对象的原型设置为null,从而从原型链中删除Object.prototype

A978-1-4842-0713-0_4_Fig1_HTML.jpg

图 4-1。

The Prototype Chain for the point and coloredPoint Objects

在前面的例子中,您看到两个对象共享相同的属性,名为xy。你并不真的想要共享对象的状态。通常,您希望每个对象都有自己的状态。您可以通过在coloredPoint对象中重新声明xy属性来解决这个问题。如果在coloredObject上设置xy属性,coloredObject将获得自己新的xy属性。以下内容说明了这一点:

var point = {x: 10,

y: 20,

print: function() {

printf("(%d, %d)", this.x, this.y);

}

};

var coloredPoint = {color: "red",

print: function() {

printf("(%d, %d, %s)", this.x, this.y, this.color);

}

};

Object.setPrototypeOf(coloredPoint, point);

// Call the print() methods of both objects

point.print();

coloredPoint.print();

// Change the x and y values in the point object

point.x = 100;

point.y = 200;

// Add own x and y properties to the coloredPoint object

coloredPoint.x = 300;

coloredPoint.y = 400;

// Call the print() methods of both objects

point.print();

coloredPoint.print();

(10, 20)

(10, 20, red)

(100, 200)

(300, 400, red)

Tip

原型链是在读取对象的属性时搜索的,而不是在更新它们时。回想一下,更新不存在的属性会向设置了该属性的对象添加一个新属性。

在前面的例子中,您看到了原型继承链在 Nashorn 中是如何工作的。您使用对象文字创建了对象,并使用Object.setPrototypeOf()函数设置了对象的原型。使用函数构造函数,您也可以做同样的事情,尽管有点不同。Nashorn 中的函数也是一个对象,默认情况下,函数的原型被设置为Function.prototype对象。Function.prototypeObject.prototype为原型。Function.prototype包含几个有用的方法和属性,可以用于所有的函数。例如,它覆盖了Object.prototypetoString()方法,该方法打印该函数的源代码。下面的代码演示了这一点:

// Create a function object called log

function log(str) {

print(new Date() + ": " + str);

}

// Call toString() method on the log object

print(log.toString());

function log(str) {

print(new Date() + ": " + str);

}

Nashorn 中的每个函数都有一个名为prototype的属性,这是另一个对象。不要混淆函数的prototype属性和函数的原型对象(或简单的原型)。函数的prototype属性被自动设置为所有使用该函数作为构造函数创建的对象的原型。prototype属性有一个名为constructor的属性,它引用回函数本身。

清单 4-9 创建了一个名为Point的构造函数,并在其prototype属性中添加了两个名为toString()distance()的方法。

清单 4-9。Point.js 文件的内容

// Point.js

// Define the Point constructor

function Point(x, y) {

this.x = x;

this.y = y;

}

// Override the toString() method in Object.prototype

Point.prototype.toString = function() {

return "Point(" + this.x + ", " + this.y + ")";

};

// Define a new method called distance()

Point.prototype.distance = function(otherPoint) {

var dx = this.x - otherPoint.x;

var dy = this.y - otherPoint.y;

var dist = Math.sqrt(dx * dx + dy * dy);

return dist;

};

清单 4-10 中的代码使用了Point构造函数来创建两个对象并计算它们之间的距离。

清单 4-10。PointTest.js 文件的内容

// PointTest.js

load("Point.js");

// Create two Point object, compute the distnce

// between them, and print the results

var p1 = new Point(100, 200);

var p2 = new Point(-100, -200);

var dist = p1.distance(p2);

printf("Distance between %s and %s is %.2f.", p1.toString(), p2.toString(), dist);

Distance between Point(100, 200) and Point(-100, -200) is 447.21.

如果你创建一个Point对象,它的原型将自动被设置为Point.prototype对象,所以新对象将继承toStringdistance方法。以下代码片段演示了这一点:

// Create two Point objects, compute and print the distnce between them

var p1 = new Point(100, 200);

var p2 = new Point(-100, -200);

var dist = p1.distance(p2);

printf("Distance between %s and %s is %.2f.", p1.toString(), p2.toString(), dist);

Distance between Point(100, 200) and Point(-100, -200) is 447.21

清单 4-11 包含了ColoredPoint构造函数的代码。上市后我会对此进行更多的解释。

清单 4-11。ColoredPoint.js 文件的内容

// ColoredPoint.js

load("Point.js");

function ColoredPoint(x, y, color) {

// Call the Point constructor function binding this,

// which is the current ColoredPoint object context

// as this for the Point invocation, so the x and y

// properties will be added to the current ColoredPoint object

Point.call(this, x, y);

// Add a color property to the new object

this.color = color;

};

// Set a new object whose prototype is Point.prototype as

// the prototype for the ColoredPoint function

ColoredPoint.prototype = Object.create(Point.prototype);

// Set the constructor property of the prototype

ColoredPoint.prototype.constructor = ColoredPoint;

// Override the toString() method of the Point.prototype object

ColoredPoint.prototype.toString = function() {

return "ColoredPoint(" + this.x + ", " + this.y + ", " + this.color + ")";

};

首先,代码加载包含Point构造函数定义的Point.js文件。

ColoredPoint构造函数中的第一条语句调用了传递xy参数的Point()函数。理解这句话的意图很重要。这与在 Java 中从子类构造函数调用超类构造函数是一样的。你正在创建一个ColoredPoint对象,你想调用Point函数作为构造函数,所以被创建的ColoredPoint对象首先被初始化为Point。你可能很想用Point(x, y)而不是Point.call(this, x, y)。然而,这是行不通的。困惑来自于关键字this的含义。当您从ColoredPoint函数调用Point()函数时,您需要将Point()函数中的关键字this指向正在创建的新的ColoredPoint对象。调用Point(x, y)时,Point()函数内的关键字this指向全局对象,而不是正在创建的ColoredPoint对象。函数的call()方法让您将this引用作为第一个参数传递。在这种情况下,您传递了在ColorPoint()函数中可用的this引用,因此Point()函数在与ColoredPoint函数相同的上下文中执行。Point()函数中的语句this.x  = xthis.y = y会将xy属性添加到新的ColoredPoint对象中。

现在,您需要为将使用ColoredPoint函数创建的对象设置原型链。这是通过将ColoredPoint函数的prototype属性设置为原型为Point.prototype的对象来实现的。Object.create()函数是这样做的,就像这样:

// Set a new object whose prototype is Point.prototype as the prototype for the

// ColoredPoint function

ColoredPoint.prototype = Object.create(Point.prototype);

该语句替换了ColoredPoint函数的prototype属性,这也将重置prototype属性的constructor属性。以下语句恢复了constructor属性:

// Set the constructor property of the prototype

ColoredPoint.prototype.constructor = ColoredPoint;

ColoredPoint对象将从Point.prototype继承toString()方法。确保在调用toString()方法时ColoredPoint返回它的颜色分量。为了实现这一点,您覆盖了toString()方法,如下所示:

// Override the toString() method of the Point.prototype object

ColoredPoint.prototype.toString = function() {

return "ColoredPoint(" + this.x + ", " + this.y + ", " + this.color + ")";

};

清单 4-12 显示了测试PointColoredPoint函数的代码。它创建每种类型的对象,并计算它们之间的距离。最后,打印结果。

清单 4-12。ColoredPointTest.js 文件的内容

// ColoredPointTest.js

load("Point.js");

load("ColoredPoint.js")

// Create a Point and a ColoredPoint objects

var p1 = new Point(100, 200);

var p2 = new ColoredPoint(25, 50, "blue");

// Compute the distance between two points

var p1Top2 = p1.distance(p2);

var p2Top1 = p2.distance(p1);

printf("Distance of %s from %s = %.2f", p1.toString(), p2.toString(), p1Top2);

printf("Distance of %s from %s = %.2f", p2.toString(), p1.toString(), p2Top1);

Distance of Point(100, 200) from ColoredPoint(25, 50, red) = 167.71

Distance of ColoredPoint(25, 50, red) from Point(100, 200) = 167.71

图 4-2 描绘了PointColoredPoint函数及其对象的原型链。乍一看,这个数字似乎有些吓人。一旦你理解了使用函数的__proto__属性和prototype属性链接一个对象和它的原型,剩下的就是通过引用链接对象了。

A978-1-4842-0713-0_4_Fig2_HTML.jpg

图 4-2。

The Prototype Chain for the Point and ColoredPoint Functions and Their Objects

使用 Object.create()方法

Object.create()方法是通过指定对象的原型和初始属性来创建对象的另一种方法。它的语法是:

Object.create(prototypeObject, propertiesObject)

该函数返回新对象的引用。prototypeObject是被设置为新对象的原型的对象或nullpropertiesObject是可选的;它是一个属性描述符,包含新对象的初始属性列表。下面两个语句是等价的。两者都创建原型为Object.prototype的空对象:

var obj1 = {};

var obj2 = Object.create(Object.prototype);

以下语句创建一个空对象,以null作为其原型:

var obj = Object.create(null);

清单 4-13 创建了一个对象来表示 2D 平面上的原点。它使用Point.prototype作为新对象的原型。它为对象指定了xy属性。注意,writableenumerableconfigurable属性在属性描述符中默认设置为false。因此,origin2D对象是不可改变的。代码还打印了新对象的属性,证明了xy属性是不可数的。注意,新对象从Point.prototype对象继承了toStringdistance属性。

清单 4-13。使用 Object.create()创建对象

// ObjectCreate.js

load("Point.js");

// Create a non-writable, non-enumerable, non-configurable

// Point to represent the origin

var origin2D = Object.create(Point.prototype, {x:{value:0}, y:{value:0}});

print("After creating:", origin2D);

// Cannot change x and y properties of origin2D.

// They are non-writable by default.

origin2D.x = 100; // No effect

origin2D.y = 100; // No effect

print("After changing x and y:", origin2D);

print("origin2D instanceof Point = " + (origin2D instanceof Point));

print("origin2D properties:");

for(var x in origin2D) {

print(x);

}

After creating: Point(0, 0)

After changing x and y: Point(0, 0)

origin2D instanceof Point = true

origin2D properties:

toString

distance

绑定对象属性

Nashorn 允许您使用Object.bindProperties(target, source)方法将一个对象的属性绑定到另一个对象,其中target是其属性将被绑定到source属性的对象。清单 4-14 显示了如何将一个对象的属性绑定到另一个对象。

清单 4-14。将一个对象的属性绑定到另一个对象

// bindproperties.js

var trg = {};

var src = {x:100, y:200, z:300};

// Bind properties of sourceObject to targetObject

Object.bindProperties(trg, src);

// Print properties using trg

printf("trg-1: x=%d, y=%d,z=%d", trg.x, trg.y, trg.z);

// Now change z using trg

trg.z = 30;  // Using src.z = 30 will have the same efect

// Print the properties using both objects and both

// should have the new value of z as 30

printf("trg-2: x=%d, y=%d,z=%d", trg.x, trg.y, trg.z);

printf("src-2: x=%d, y=%d,z=%d", src.x, src.y, src.z);

// Now add a new property to trg

trg.u = 30;

// At this point, trg has the property named u, but src does not.

printf("trg-3: x=%d, y=%d,z=%d, u=%d", trg.x, trg.y, trg.z, trg.u);

printf("src-3: x=%d, y=%d,z=%d, u=%s", src.x, src.y, src.z, src.u);

// Now add a new property to src

src.v = 30;

// Contrary to our expectation, trg.v does not exist. It may indicate a bug.

printf("trg-4: x=%d, y=%d,z=%d, u=%d, v=%s", trg.x, trg.y, trg.z, trg.u, trg.v);

printf("src-4: x=%d, y=%d,z=%d, u=%s, v=%d", src.x, src.y, src.z, src.u, src.v);

trg-1: x=100, y=200,z=300

trg-2: x=100, y=200,z=30

src-2: x=100, y=200,z=30

trg-3: x=100, y=200,z=30, u=30

src-3: x=100, y=200,z=30, u=undefined

trg-4: x=100, y=200,z=30, u=30, v=undefined

src-4: x=100, y=200,z=30, u=undefined, v=30

Object.bindProperties()方法有两种实际用途:

  • 枚举 Nashorn 脚本中 Java 对象的属性
  • 您可以将对象的属性导入全局范围,就像这些属性是全局声明的一样

在 Nashorn 中,不能使用for..infor..each..in语句枚举 Java 对象的属性。注意,我们讨论的是 Java 对象,而不是 Nashorn 对象。解决方案是将 Java 对象的属性绑定到 Nashorn 对象,并在 Nashorn 对象上使用这些迭代语句之一。清单 4-15 显示了如何迭代java.util.HashSet对象的属性。

清单 4-15。迭代 Java 对象的属性

// bindjavaobject.js

var trg = {};

var src = new java.util.HashSet();

// Try iterating src properties

print("Iterating over src properties:");

for (var propName in src) {

print(propName); // Will not get here

}

// Bind properties of the java.util.HashSet object referenced by src to trg

Object.bindProperties(trg, src);

// Try iterating src properties

print("Iterating over trg properties:");

for (var propName in trg) {

print(propName);

}

Iterating over src properties:

Iterating over trg properties:

getClass

wait

notifyAll

notify

remove

removeAll

iterator

stream

hashCode

toArray

parallelStream

add

spliterator

forEach

containsAll

clear

isEmpty

removeIf

contains

size

addAll

equals

clone

toString

retainAll

class

empty

清单 4-16 显示了创建一个 logger 对象的代码,该对象只有一个名为log的属性,即一个函数。它使用Object.bindProperties()方法将对象的属性绑定到由this引用的全局对象。一旦绑定发生,您就可以像使用全局函数一样使用函数log()。当加载代码时,它打印日志文件的位置。

清单 4-16。将对象的属性绑定到全局对象

// loadglobals.js

// Create a function obejct using a function expression

var Logger = function () {

var LOG_FILE = new java.io.File("nashorn_app.log");

var printer = new java.io.PrintWriter(

new java.io.FileWriter(LOG_FILE, true /*append*/), true /*autoflush*/);

this.log = function (msg) {

printer"println(String)" + ": " + msg);

};

// Print the path for the log file

print("Logs using the log(msg) global function will be written to " +

LOG_FILE.absolutePath);

};

// Bind a Logger object to the global object.

// Here, this refers to the global object

Object.bindProperties(this, new Logger());

下面的代码加载loadglobals.js文件并使用log()函数,就像它是一个全局函数一样。您可能会得到不同的输出。记录的消息将存储在输出中打印的文件中。您可以使用这种技术在您需要的全局范围内导入尽可能多的全局对象。您需要做的就是将所有这些属性添加到一个对象中,并将该对象绑定到全局对象this:

// Load the globals

load("loadglobals.js");

// Now you can use the log() method as a global function

log("Hello logger!");

Logs using the log(msg)  global function will be written to C:\nashorn_app.log

锁定对象

Object提供了几种锁定对象的方法。这些方法使您可以更好地控制锁定对象的哪些部分或功能:

  • 您可以阻止对象的扩展(或使其不可扩展),这样就不能向其添加新属性
  • 您可以密封对象,这样既不能删除现有属性,也不能添加新属性
  • 您可以冻结对象,以便不能删除现有特性,不能添加新特性,并且特性是只读的

Object.preventExtensions(obj)方法让你使指定的对象不可扩展。Object.seal(obj)方法密封指定的对象。方法冻结了指定的对象。请注意,一旦您将一个对象设置为不可扩展、密封或冻结,就没有办法逆转这些属性。您可以分别使用Object.isExtensible(obj)Object.isSealed(obj)Object.isFrozen(obj)方法来检查指定的对象是否是可扩展的、密封的和冻结的。在严格模式下,如果试图对对象进行不允许的更改,则会引发错误;例如,向不可扩展的对象添加新属性会引发错误。

Tip

防止扩展、密封和冻结对象只会影响该对象,而不会影响其原型。仍然可以对其原型进行更改,并且该对象将继承这些更改。如果要完全冻结对象,需要冻结对象原型链上的所有对象。

清单 4-17 显示了如何防止一个对象的扩展。它还展示了您可以向不可扩展对象的原型添加属性,并且该对象将继承该属性。代码使point对象不可扩展,添加名为z的属性不会将属性添加到对象中。然而,您可以向Object.prototype添加相同的属性,然后point对象继承它。

清单 4-17。防止对象扩展

// preventextensions.js

var point = {x:1, y:1};

print("isExtensible() = " + Object.isExtensible(point));

// Make point non-extensible

Object.preventExtensions(point);

print("isExtensible() = " + Object.isExtensible(point));

// Try adding a new property to point

point.z = 1; // Will throw an error in strict mode

// Check if the property z was added to point

print("point has property z = " + ("z" in point));

// point inherits from Object.prototype. Let us add a property

// named z in Object.prototype

Object.prototype.z = 1;

// Check if the property z was added to point. Now point inherits

// the proeprty named z from Object.prototype.

print("point has property z = " + ("z" in point));

// The following statement has no effect as the point object

// is still non-extensible.

point.z = 100;

print("z = " + point.z); // Reads the Object.prototype.z

isExtensible() = true

isExtensible() = false

point has property z = false

point has property z = true

z = 1

访问缺失的属性

Nashorn 提供了一个扩展,允许您向对象添加一个方法,当访问一个不存在的对象属性时调用该方法。您也可以为不存在的方法提供这样的方法。属性名为__noSuchProperty____noSuchMethod__。注意属性名前后的两个下划线。这两个属性都是函数。如果你正在做一个项目,并且到处都需要这种行为,你应该把这些钩子添加到Object.prototype,这样它们在默认情况下在所有对象中都是可用的。清单 4-18 显示了如何将这些属性添加到Object.prototype中。向__noSuchProperty__的函数传递被访问的属性名。向__noSuchMethod__的函数传递方法名和方法调用中使用的所有参数。您可以使用函数中的arguments对象来访问所有参数。

清单 4-18。添加缺少的属性和方法挂钩

// missingprops.js

var point = {x:10, y:20};

// Add no such property and no such method hooks

Object.prototype.__noSuchProperty__ = function (prop) {

throw Error("No such property: " + prop);

};

Object.prototype.__noSuchMethod__ = function () {

var desc = arguments[0] + "(";

if (arguments.length > 1) {

desc += arguments[1];

}

for (var i = 2; i < arguments.length; i++) {

desc += ", " + arguments[i];

}

desc += ")";

throw Error("No matching method found: " + desc);

};

// Try accessing point.z. Will throw an Error.

try {

var z = point.z;

}

catch(e) {

print(e.message);

}

// try calling a ono-existent method named dustance

try {

point.distance(10, 20);

}

catch(e) {

print(e.message);

}

No such property: z

No matching method found: distance(10, 20)

序列化对象

Nashorn 使用名为JSON的内置对象支持对象的序列化和反序列化。JSON 也是代表 JavaScript 对象符号的首字母缩写词,它指定了 JavaScript 对象如何转换为字符串,反之亦然。JSON 是 RFC 4627 在 http://www.ietf.org/rfc/rfc4627.txt 中描述的一种数据交换格式。JSON对象包含两个方法:

  • JSON.stringify(value, replacer, indentation)
  • JSON.parse(text, reviver)

方法将一个 Nashorn 值转换成一个字符串。JSON.parse()方法将 JSON 格式的字符串转换成 Nashorn 值。

stringify()方法中的value参数可以是一个对象,也可以是一个原始值。并非所有类型的值都可以字符串化。例如,undefined不能串化。NaNInfinity串成null

replacer参数是一个function或者一个数组。如果replacer是一个函数,它有两个参数。第一个参数是被字符串化的属性的名称,第二个参数是属性的值。函数的返回值被用作最终的字符串值。如果函数返回undefined或没有值,则属性不被字符串化。第一次调用replacer时,空字符串作为属性名,被字符串化的对象作为值。后续调用传递被字符串化的属性的属性名和属性值。如果replacer是一个数组,它包含要被字符串化的属性的名称。任何没有出现在数组中的属性都不会被字符串化。

indentation是用于在输出中缩进的数字或字符串。如果是数字,则是用于缩进的空格数。其价值上限为 10 英镑。如果是"\t"这样的字符串,则用于每一级的缩进。replacerindentation参数在stringify()方法中是可选的。

reviver是一个函数,当 JSON 文本被解析成一个对象时会调用这个函数。它需要两个参数。第一个参数是被解析的属性的名称,第二个参数是属性的值。如果函数返回nullundefined,则相应的属性被删除。

清单 4-19 展示了JSON对象的使用。它字符串化一个对象。在字符串化过程中,它将属性值乘以 2。在解析期间,它应用反向逻辑来恢复对象。

清单 4-19。使用 JSON 对象来字符串化和解析对象

// json. js

var point = {x: 10, y: 20};

function replacer (key, value) {

if (key === "x" || key === "y") {

// Multiply the value by 2

return value * 2;

}

else {

// Simply return the value

return value;

}

}

function reviver (key, value) {

if (key === "x" || key === "y") {

// Divide the value by 2

return value / 2;

}

else {

return value;

}

}

var pointString = JSON.stringify(point, replacer, "  ");

print("Stringified object is");

print(pointString);

var obj = JSON.parse(pointString, reviver);

print("Parsed object proeprties are");

for(var prop in obj) {

print("obj[" + prop + "] =  " + obj[prop]);

}

Stringified object is

{

"x": 20,

"y": 40

}

Parsed object proeprties are

obj[x] =  10

obj[y] = 20

动态评估脚本

Nashorn 包含一个名为eval(string)的全局函数。它将包含 Nashorn 代码的字符串作为参数,执行代码,并返回代码中最后计算的值。下面的代码片段展示了一个使用eval()函数将两个数字相加的简单示例:

// Assigns 300 to sum

var sum = eval("100 + 200");

您不会使用eval()函数来进行如此琐碎的计算。Nashorn 可以在不使用eval()函数的情况下执行 100 + 200 这样的算术表达式。当您在运行时获得字符串形式的 Nashorn 代码时,您将使用它。

eval()函数在调用者的上下文中执行。如果从全局上下文中调用它,它将在全局上下文中执行。如果它是从函数中调用的,它将在函数的上下文中执行。

清单 4-20 展示了eval()函数执行的上下文。第一次,它在全局上下文中被调用,因此它定义了一个新变量z,并在全局上下文中将它设置为 300。它从全局上下文中读取x的值,将其乘以 2,并返回设置为y的 200。第二次,从函数中调用它,它作用于函数的局部变量xyx

清单 4-20。EvalTest.js 文件的内容

// EvalTest.js

var x = 100;

var y = eval("var z = 300; x * 2;"); // Called from the global context

print(x, y, z);

function testEval() {

var x = 300;

// Called from the function context

var y = eval("var z = 900; x * 2;");

print(x, y, z);

}

testEval();

100 200 300

300 600 900

变量范围和提升

Java 支持块级作用域。块中声明的变量是该块的局部变量,在该块前后都不可访问。Nashorn(和所有 JavaScript 实现)支持两个范围:

  • 全球范围
  • 功能范围

全局代码中的变量和函数声明在全局范围内随处可见。类似地,函数中的变量和函数声明在函数中随处可见。

考虑清单 4-21 中的代码及其输出。如果您以前没有使用过 JavaScript,输出会让您大吃一惊。

清单 4-21。纳斯霍恩的申报吊装

// beforehoisting.js

// Try accessing empId variable before declaring it

print("1\. empId = " + empId);

// Declare and initialize empId now

var empId = 100;

function test() {

if (empId === undefined) {

var empId = 200;

}

print("2\. empId = " + empId);

}

// Call the test function

test();

print("3\. empId = " + empId);

1\. empId = undefined

2\. empId = 200

3\. empId = 100

第一个输出显示您可以在声明变量之前访问它。变量值为undefined。在test()函数中,你会期望表达式empId === undefined会从全局范围读取empId的值,并且它会对false求值。但是,输出显示的却不是这样。第二个输出显示了test()函数中的if条件被评估为true,并且empId局部变量被设置为 200。第三个输出显示函数调用没有影响全局变量empId,它的值仍然是 100。

您可以通过两种方式理解此变量的作用域行为:

  • 作为一个两阶段的过程:首先,处理所有的声明。第二,执行代码。因为所有声明都是在执行代码之前处理的,所以在执行代码时,所有变量和函数声明都是可用的,而不管它们是在哪里声明的。当然,您需要考虑这些声明的全局和函数范围。
  • As 声明提升:这仅仅意味着所有的变量和函数声明都被移动到各自作用域的顶部。这相当于说你已经在全局和函数作用域的顶部声明了所有的变量和函数。

清单 4-22 显示了与清单 4-21 相同的代码,但是在声明被提升之后。这一次,您在理解输出方面不会有任何问题。

清单 4-22。声明提升后重新组织代码

// afterhoisting.js

var empId; // Hoisted

function test() {

var empId; // Hoisted

if (empId === undefined) {

empId = 200; // Left after hoisting

}

print("2\. empId = " + empId);

}

// Try accessing empId variable before declaring it

print("1\. empId = " + empId);

// Declare and initialize empId now

empId = 100; // Left after hoisting

// Call the test function

test();

print("3\. empId = " + empId);

1\. empId = undefined

2\. empId = 200

3\. empId = 100

请注意,提升发生在函数声明中,而不是函数表达式中,这使您能够在声明函数之前调用函数。还有一点要记住:只提升变量声明,而不是变量声明中的赋值部分,如清单 4-22 所示。

使用严格模式

您可以通过使用"use strict"指令在 Nashorn 脚本中使用严格模式。它用在全局代码的开头或函数代码的开头。如果在全局代码的开头使用它,所有代码都被认为是以字符串模式执行的。如果它出现在函数代码的开头,那么只有该函数代码在严格模式下执行。您也可以在由eval()函数执行的脚本中使用"use strict"指令。

以下代码片段将"use strict"指令用于全局范围:

"use strict";

// An error. Using a variable without declaring it with var

x = 10;

// other code goes here

当调用test()函数时,下面的代码将抛出一个错误,因为该函数使用了"use strict"指令,并且它使用了一个名为y的变量,而没有用关键字var声明它:

x = 10; // OK. The global code is not in strict mode

function test() {

"use strict";

y = 100; // Will cause an error when executed

}

test();

内置全局对象

Nashorn 定义了许多内置对象。我将在随后的章节中讨论其中的一些。

对象对象

Nashorn 提供了一个名为Object的内置对象。你可以把它想成类似于 Java 中的java.lang.Object类。Object在 Nashorn 中充当其他对象的基础对象。它有几个有用的属性,用于处理所有类型的对象。我已经在前面的章节中讨论了其中的许多。

Object是一个函数,也可以用作构造函数。它接受一个可选参数。如果你调用Object函数,它与用new操作符调用Object构造函数的效果相同。如果将一个对象传递给函数,它将返回对同一对象的引用。如果传递一个原始值,它将返回一个包装原始值的包装器对象。您可以在返回的对象上使用valueOf()方法获得原始值。不带参数调用函数会创建一个空对象。以下代码说明了如何使用Object:

var str = "Hello";

// Assigns a wrapper for str to p1 because str is a primitive type

var p1 = new Object(str);

print("p1.valueOf() is", p1.valueOf());

print("typeof p1 is", typeof p1);

print("p1.valueOf() === str is", p1.valueOf() === str);

var msg = new String("Hello");

// Assigns the reference of msg to p2\. new Object(msg) returns

// the reference of msg because msg is an object.

var p2 = new Object(msg);

print("p2 === msg is", p2 === msg);

print("p2.valueOf() is", p2.valueOf());

print("typeof p2 is", typeof p2);

print("p2.valueOf() === msg is", p2.valueOf() === msg);

p1.valueOf() is Hello

typeof p1 is object

p1.valueOf() === str is true

p2 === msg is true

p2.valueOf() is Hello

typeof p2 is object

p2.valueOf() === msg is false

表 4-11 包含了Object的属性和方法列表及其描述。

表 4-11。

The List of Properties and Methods of Object with Their Descriptions

属性/方法 描述
Object.prototype 它是Object的原型对象。其writableenumerableconfigurable属性被设置为false
Object.prototype.constructor 包含对创建对象的constructor函数的引用
Object.create(p, descriptor) 创建一个新对象,将指定的p设置为原型,并将descriptor中的所有属性添加到新对象中。descriptor参数是可选的
Object.defineProperties(obj, descriptors) 创建或更新指定对象的属性
Object.defineProperty(obj, prop, descriptor) 用指定对象的创建或更新指定属性
Object.freeze(obj) 通过使指定对象不可扩展、不可配置并使其所有属性为只读来冻结该对象
Object.getOwnPropertyDescriptor(obj, prop) 返回指定对象的指定属性的属性说明符
Object.getOwnPropertyNames(obj) 返回一个数组,包含指定对象的所有自身属性的名称。该数组还包括不可数的属性
Object.getPrototypeOf(obj) 返回指定对象的原型
Object.isExtensible(obj) 返回指定对象的true是可扩展的,否则返回false
Object.isFrozen(obj) 返回指定对象的true被冻结,否则返回false
Object.isSealed(obj) 返回指定对象的true是密封的,否则返回false
Object.keys(obj) 返回一个数组,其中包含指定对象的所有可枚举、非继承属性的名称
Object.preventExtensions(object) 使指定的对象不可扩展,以便不能向该对象添加新属性
Object.prototype.hasOwnProperty(prop) 如果对象将指定的属性作为自己的属性,则返回true;否则返回false
Object.prototype.isPrototypeOf(obj) 如果指定的对象是调用了isPrototypeOf()方法的对象的原型,则返回true
Object.prototype.propertyIsEnumerable(prop) 如果指定的属性是可枚举的,则返回true;否则返回false
Object.prototype.toLocaleString() 返回了对象的本地化字符串表示形式。它旨在被对象重写。默认情况下,它返回一个由toString()方法返回的字符串
Object.prototype.toString() 返回对象的字符串表示形式。它旨在被对象重写。默认情况下,它返回一个字符串[object class],其中class是对象的类名,例如StringDateObject等等
Object.prototype.valueOf() 如果对象包装了一个原始值,它将返回该原始值。否则,它返回对象本身
Object.seal(obj) 密封指定的对象
Object.setPrototypeOf(obj, proto) 将指定的proto设置为指定的obj的原型

功能对象

Function对象是一个可以作为函数或构造函数调用的函数。将其作为函数调用与作为构造函数调用的方式相同。它从一个字符串创建一个新的函数对象。它接受可变数量的参数,并具有以下签名:

Function(p1, p2, p3,..., body);

这里,p1p2p3等是新函数的形参名称,body是函数体。最后一个参数总是函数的主体。如果在没有任何参数的情况下调用它,它将创建一个没有形参的空函数和一个空体。下面的代码片段创建了一个函数,它接受两个参数,并返回对两个参数应用+运算符的结果。调用adder(10, 20)将返回 30:

var adder = new Function("p1", "p2", "return p1 + p2");

Function.prototype是一个本身是一个Function对象的对象。Function.prototype被设置为您创建的所有函数的原型对象。因此,Function.prototype中可用的所有属性在所有函数中都可用。以下属性在Function.prototype中定义:

  • Function.prototype.constructor
  • Function.prototype.toString()
  • Function.prototype.apply(thisArg, argArray)
  • Function.prototype.call(thisArg, arg1, arg2,...)
  • Function.prototype.bind(thisArg, arg1, arg2,...)

Function.prototype.constructor属性被设置为内置的Function构造函数。

Function.prototype.toString()方法返回函数对象的依赖于实现的表示。下面的代码演示如何使用此方法以及它为不同类型的对象返回的值。注意,对于用户定义的函数,它以字符串形式返回函数声明,对于内置函数,函数体以[native code]形式返回:

function adder(n1, n2) {

return n1 + n2;

}

// Call the toString() method of the user-defined adder function

var adderStr = adder.toString();

// Call the toString() method of the built-in print function

var printStr = print.toString;

print("adderStr: \n" + adderStr);

print("printstr:\n" + printStr);

adderStr:

function adder(n1, n2) {

return n1 + n2;

}

printstr:

function toString() { [native code] }

apply()call()方法用于相同的目的。两者都用于调用对象的方法。它们的区别仅在于参数类型。这两个方法的第一个参数都是调用该方法的上下文中的一个对象。apply()方法让您在一个数组中指定方法的参数,而call()方法让您分别指定每个参数。这些方法允许您在任何其他对象上调用一个对象的方法。您可以使用这些方法来调用对象的重写方法。

下面的代码展示了如何在一个String对象上调用Object.prototypetoString()方法:

// Create a String object

var str = new String("Hello");

// Call the toString() method of the String object

var stringToString = str.toString();

// Call the toString() method of the Object.prototype object on str

var objectToString = Object.prototype.toString.call(str);

print("String.toString(): " + stringToString);

print("Object.prototype.toString(): " + objectToString);

String.toString(): Hello

Object.prototype.toString(): [object String]

bind()方法将一个函数绑定到一个对象,并返回一个新的函数对象。当调用新函数时,它在绑定对象的上下文中执行。它采用可变长度的参数。第一个参数是要与函数绑定的对象。其余的参数是您希望与函数绑定的参数。如果将任何参数绑定到该函数,则在调用绑定的函数时将使用这些参数。

假设您想要为Point对象创建一个函数,该函数将计算Point到原点的距离。您可以创建一个Point来表示原点,并将Point.prototype.distance函数绑定到这个对象来获得一个新函数。清单 4-23 展示了这项技术。

清单 4-23。将函数绑定到对象

// functionbind.js

load("Point.js");

// Create a 2D origin

var origin = new Point(0, 0);

// Create a new method called distanceFromOrigin() by binding

// the distance() method of the Point to the origin object

var distanceFromOrigin = Point.prototype.distance.bind(origin);

var dist = distanceFromOrigin(new Point(0, 10));

print(dist);

// The above distanceFromOrigin() is the same as follows

var dist2 = origin.distance(new Point(0, 10));

print(dist2);

10

10

关于Function()构造函数的重要一点是,它创建的函数不使用词法范围;相反,它们总是像顶级函数一样被编译。清单 4-24 展示了这一微妙之处。

清单 4-24。测试由函数对象创建的函数的捕获范围

// functioncapture.js

var empId = 100; // Global

function createFunction1() {

var empId = 200;  // Local

// Does not capture local empId

var test = new Function("print(empId)");

return test;

}

function createFunction2() {

var empId = 200; // Local

function test () {

print(empId); // Captures local empId

}

return test;

}

createFunction1()(); // Prints 100 (the global empId)

createFunction2()(); // Prints 200 (the local empId)

100

200

createFunction1()使用Function对象创建一个新函数时,这个新函数不捕获局部empId,而是捕获全局empId。然而,当在createFunction2()函数中声明了一个嵌套函数时,新函数会捕获局部empId。输出验证了该规则。

字符串对象

字符串对象是一个函数。它可以简单地用作函数或构造函数。当它用作函数时,它将参数转换为原始类型的字符串值。下面是一个使用String作为函数的例子:

var str1 = String(100); // Returns "100"

var str2 = String();    // Returns ""

String被用作构造函数时,它创建一个新的String对象,其内容作为传递的参数:

// Create a String object

var str = new String("Hello");

String 对象中的每个字符都被分配了一个索引。第一个字符的索引为 0,第二个字符的索引为 1,依此类推。每个 String 对象都有一个名为length的只读属性,它包含字符串中的字符数。

包含许多有用的方法来操作字符串对象,并以不同的方式获取其内容。表 4-12 列出了这些方法。请注意,您可以对原始字符串值使用所有字符串对象方法;在应用该方法之前,原始字符串将自动转换为字符串对象。

表 4-12。

The List of Methods of in String.prototype with Their Descriptions

方法 描述
charAt(index) 将指定的index处的字符作为原始字符串值返回
charCodeAt(index) 返回指定index处字符的 Unicode 值
concat(arg1, arg2, ...) 返回连接对象内容和指定参数的原始字符串
indexOf(substring, start) 返回子字符串在对象中第一次出现的索引。搜索从start开始。如果start未通过,则从索引 0 开始搜索
lastIndexOf(substring, start) 它的工作方式与indexOf()方法相同,只是它从字符串的末尾搜索substring
localeCompare(target) 返回一个数字。如果字符串小于target,则返回负数;如果 string 大于target,则返回正数;否则,它返回零
match(regexp) 在字符串中搜索指定的正则表达式regexp,并以数组形式返回结果
replace(regexp, replacement) 使用正则表达式regexp搜索字符串,用替换项替换匹配的子字符串,并将结果作为原始字符串值返回
search(regexp) 在字符串中搜索指定的正则表达式,并返回第一个匹配项的索引。如果没有找到匹配项,则返回–1
slice(start, end) 返回包含字符串中的startend索引中的字符的子字符串,其中start是包含性的,end是排他性的。如果没有指定end,则返回从start开始的所有字符。如果end为负,则从末尾开始计算索引。"Hello".slice(1, 2)返回"e""Hello".slice(1, -2)返回"el"
split(delimiter, limit) 通过拆分指定delimiter周围的字符串返回一个数组。limit是可选的,它指定数组中返回的元素的最大数量
substring(start, end) 返回从start开始到end的子字符串。如果其中一个参数为NaN或负数,它将被替换为零。如果它们大于字符串的长度,它们将被替换为字符串的长度。如果start大于end,它们被交换。"Hello".slice(1, 2)返回"e"并且"Hello".slice(1, -2)返回"H"
toLowerCase() 通过用相应的小写字符替换大写字符,返回字符串的小写等效项
toLocaleLowerCase() toLowerCase()的工作方式相同,除了它使用特定于地区的大写到小写字符映射
toUpperCase() 通过用相应的大写字符替换小写字符,返回字符串的大写等效项
toLocaleUpperCase() toUpperCase()的工作方式相同,除了它使用特定于地区的小写到大写的字符映射
toString() 以原始字符串值的形式返回字符串对象的内容
trim() 通过删除字符串中的空格来返回原始字符串值
valueOf() toString()

数字对象

Number对象是一个函数。它可以作为函数或构造函数调用。当作为函数调用时,它将提供的参数转换为原始数值。如果不带参数调用它,它将返回原始数字零。当它作为构造函数被调用时,它返回一个包装了指定参数的 Number 对象。您可以将 Number 对象看作 Java 中的包装器对象IntegerLongFloatDoubleShortByte类型。Number 对象的valueOf()方法返回存储在对象中的原始数值。以下代码显示了如何将 Number 对象用作函数和构造函数:

// Converts the string "24" to a number 24 and assigns 24 to n1

var n1 = Number("24");

// Asigns zero to n2

var n2 = Number();

// Create a Number object for the nuembr 100.43

var n3 = new Number(100.43);

printf("n1 = %d, n2 = %d, n3 = %f", n1, n2, n3.valueOf());

n1 = 24, n2 = 0, n3 = 100.430000

Number 对象包含几个使用数字的有用属性。它们在表 4-13 中列出。所有列出的方法都在Number.prototype对象中定义。

表 4-13。

The List of Properties and Method of the Number Object with Their Descriptions

属性/方法 描述
Number.MAX_VALUE 它是 Number 类型的最大正的有限值。大约是 1.7976931348623157e+308
Number.MIN_VALUE 它是数字类型的最小正的有限值。大约是 4.9e-324
Number.NaN 它与代表非数字的全局变量NaN相同
Number.NEGATIVE_INFINITY 表示负无穷大。它与全局变量–Infinity相同
Number.POSITIVE_INFINITY 表示正无穷大。它与全局变量Infinity相同
toString(radix) 将数字转换为指定的radix。如果未指定基数,则使用 10。基数必须是介于 2 和 36 之间的数字,包括 2 和 36
toLocaleString() 将数字格式化为本地特定的格式,并将其作为原始字符串值返回。目前,它返回与toString()方法相同的值
valueOf() 返回存储在 Number 对象中的基元值
toFixed(digits) 返回一个数字的字符串表示形式,该数字在小数点后恰好包含指定的digits。如有必要,将数字四舍五入。如果小数点后的位数小于指定的digits,则该数字用零填充。如果该数字大于 1e+21,它将返回一个以指数表示法表示该数字的字符串。如果没有指定digits,则假定为零。digits可以在 0 到 20 之间
toExponential(digits) 以指数记数法返回数字的字符串表示形式。包含小数点前的一位数字和小数点后的digits位数。digits可以在 0 到 20 之间
toPrecision(precision) 返回带有precision有效数字的数字的字符串表示。它可能以十进制或指数记数法返回数字。如果没有指定precision,它将数字转换为字符串并返回结果

您可以在所有原始数字上调用 Number 对象的所有方法。然而,在原始数值上调用方法时必须小心。数字也可以有小数。解析器在下面的语句中变得混乱:

var x = 1969.toString(16); // A SyntaxError

解析器不知道 1969 之后的小数是数字的一部分还是调用toString()方法的点。有两种方法可以解决这个问题:

  • 你需要在数字中使用两位小数。最后一个小数将被认为是调用下面方法的点,例如1969..toString(16)
  • 用一对圆括号将数字括起来,例如(1969).toString(16)

以下代码显示了如何将 Number 对象的方法用于原始数字:

var n1 = 1969..toString(16);

print("1969..toString(16) is " + n1);

var n2 = (1969).toString(16);

print("(1969).toString(16) is " + n2);

var n3 = (1969.79).toFixed(1);

print("(1969.79).toFixed(1) is " + n3);

1969..toString(16) is 7b1

(1969).toString(16) is 7b1

(1969.79).toFixed(1) is 1969.8

布尔对象

布尔对象是一个函数。它可以作为函数或构造函数调用。它需要一个可选参数。如果作为函数调用,它会将指定的参数转换为布尔原始值(truefalse)。如果未指定参数,函数将返回false。作为构造函数,它将指定的参数包装到一个布尔对象中。请注意,如果参数不是 Boolean 类型,则首先将参数转换为 Boolean 原始值。例如,参数 100 将被转换为布尔值true并存储为true,而不是 100。它的toString()方法返回一个字符串“true”或“false”,这取决于布尔对象中包装的值。valueOf()方法返回包装在对象中的原始布尔值。布尔对象没有任何其他有趣的属性或方法。以下代码显示了如何使用布尔对象:

var b1 = Boolean(100);      // Assins true to b1

var b2 = Boolean(0);        // Assins false to b1

var b3 = Boolean();         // Assigns false to b3

var b4 = new Boolean(true); // Assign a Boolean object to b4

print("Boolean(100) returns " + b1);

print("Boolean(0) returns " + b2);

print("Boolean() returns " + b3);

print("b4.valueOf() returns " + b4.valueOf());

Boolean(100) returns true

Boolean(0) returns false

Boolean() returns false

b4.valueOf() returns true

日期对象

日期对象是一个函数。它可以作为函数或构造函数调用。它的签名是:

Date(year, month, date, hours, minutes, seconds, milliseconds)

Date 对象中的所有参数都是可选的。当作为函数调用时,它以字符串形式返回当前日期和时间。当作为函数调用它时,可以向它传递参数;但是,所有的参数总是被忽略。当它作为构造函数被调用时,参数表示日期部分,并用于初始化 date 对象。下面的代码显示了如何使用 Date 对象作为函数来获取当前日期和时间。运行代码时,您可能会得到不同的输出:

// Call Date as a function

print(Date());

print(Date(2014, 10, 4, 19, 2)); // Arguments to Date are ignored

Sat Oct 04 2014 19:07:20 GMT-0500 (CDT)

Sat Oct 04 2014 19:07:20 GMT-0500 (CDT)

Date 对象可以作为构造函数调用,以下列形式传递参数:

  • new Date()
  • new Date(milliseconds)
  • new Date(dateString)
  • new Date(year, month, day, hours, minutes, seconds, milliseconds)

如果参数的数量少于两个,则使用前三种形式之一。如果参数的数量为两个或更多,则使用第四种形式。如果没有传递参数,它将创建一个具有当前日期和时间的 Date 对象。如果只传递了一个 number 参数,则它被认为是自 UTC 1970 年 1 月 1 日午夜以来经过的毫秒数。如果只传递了一个字符串参数,则字符串中的日期和时间将被解析并用作新 date 对象的日期和时间值。如果传递两个或更多参数,则构造一个日期对象;其余值设置为 1 表示天,0 表示小时、分钟、秒和毫秒。Date 对象有一个静态方法Date.now(),该方法返回 UTC 1970 年 1 月 1 日午夜到当前时间之间经过的毫秒数。

下面的代码演示如何创建日期对象,并将不同数量的参数传递给构造函数:

// Current date and time

print("new Date() =", new Date());

// Pass 2000 milliisesonds

print("new Date(2000) =", new Date(2000));

// Pass 2000 milliisesonds and convert to UTC date time string

print("new Date(2000).toUTCString() =", new Date(2000).toUTCString());

// Pass a date as a string. Date is considered in UTC

print('new Date("2014-14-04") =', new Date("2014-14-04"));

// Pass year and month. day will default to 1

print("new Date(2014, 10) = ", new Date(2014, 10));

// Pass year, month. day, and hour. Other parts will default to 0

print("new Date(2014, 10, 4, 10) = ", new Date(2014, 10, 4, 10));

// Milliseconds elapsed from midnight Januray 1070 and now

print("Date.now() = ", Date.now());

new Date() = Sat Oct 04 2014 19:38:46 GMT-0500 (CDT)

new Date(2000) = Wed Dec 31 1969 18:00:02 GMT-0600 (CST)

new Date(2000).toUTCString() = Thu, 01 Jan 1970 00:00:02 GMT

new Date("2014-14-04") = Fri Oct 03 2014 19:00:00 GMT-0500 (CDT)

new Date(2014, 10) =  Sat Nov 01 2014 00:00:00 GMT-0500 (CDT)

new Date(2014, 10, 4, 10) =  Tue Nov 04 2014 10:00:00 GMT-0600 (CST)

Date.now() = 1412469527013

在日期对象中有超过四十种方法。我将简要地解释它们。这些方法的工作方式是相同的,只是日期和时间的组成部分不同。它包含getYear()getFullYear()getDate()getMonth()、getHours()getMinutes()getSeconds()getMilliseconds()方法,分别返回 date 对象的两位数年份、整年、日期、月、小时、分钟、秒和毫秒组成部分。它包含一个返回星期几的getDay()方法(0 表示星期天,6 表示星期六)。有相应的 setter 方法如setHours()setDate()等来设置日期和时间组件。还有另一组 getters 和 setters 处理 UTC 日期和时间;它们的名称是基于getUTCXxxsetUTCXxx的模式,如getUTCYear()getUTCHours()setUTCYears()setUTCHours()等等。getTime()setTime()方法处理自 1970 年 1 月午夜以来的毫秒数。Date对象包含几个名为toXxxString()的方法,将日期转换成特定的字符串格式。例如,toDateString()返回人类可读的日期部分的字符串格式,而toTimeString()以同样的方式工作,但是是在时间部分。

Date.parse(str)静态方法解析指定的str,将其视为YYYY-MM-DDTHH:mm:ss.sssZ格式的日期和时间,并返回自 UTC 1970 年 1 月 1 日午夜以来经过的毫秒数以及解析的日期和时间。

数学对象

Math 对象包含数学常量和函数属性。表 4-14 列出了数学对象的属性及其描述。数学对象的属性是不言自明的;我不会讨论任何使用它们的例子。

表 4-14。

The List of Properties and Methods of the Math Object

属性/方法 描述
Math.E 自然对数的底 e 的数值,大约为 2.7182818284590452354
Math.LN10 10 的自然对数的数值,约等于 2.302585092994046
Math.LN2 2 的自然对数的数值,大约为 0.6931471805599453
Math.LOG2E e 的以 2 为底的对数的数值,大约为 1.4426950408889634
Math.LOG10E e 的以 10 为底的对数的数值,大约为 0.4342944819032518
Math.PI π的数值,即圆的周长与其直径之比,大约为 3.1415926535897932
Math.SQRT1_2 0.50 的平方根的数值,大约为 0.7071067811865476
Math.SQRT2 2 的平方根的数值,大约为 1.4142135623730951
Math.abs(x) 返回 x 的绝对值。例如,Math.abs(2)Math.abs(-2)都返回 2
Math.acos(x), Math.asin(x), Math.atan(x) 返回x的反余弦、反正弦和反正切
Math.atan(y, x) 返回参数yx的商y/x的反正切值,其中yx的符号用于确定结果的象限
Math.ceil(x) 返回大于或等于 x 的最接近的整数
Math.cos(x), Math.sin(x), Math.tan(x) 返回x的余弦、正弦和正切值
Math.exp(x) 返回e的 x 次方,其中eMath.E
Math.floor(x) 返回小于或等于 x 的最接近的整数
Math.log(x) 返回 x 的自然对数。也就是说,计算 x 底数- e的对数
Math.max(v1, v2,...) 返回所有参数v1v2等的最大值,必要时将参数转换为数字类型。如果没有参数被传递,则返回–Infinity
Math.min(v1, v2,...) 返回所有参数v1v2的最小值,依此类推,必要时将参数转换为数字类型。如果没有参数被传递,则返回Infinity
Math.pow(x, y) 返回x的幂y的结果
Math.random() 返回介于 0(含)和 1(不含)之间的随机数
Math.round(x) 返回一个接近x的整数。如果两个整数同样接近,则返回更接近于+Infinity的整数。例如,Math.round(2.3)返回 2,Math.round(2.9)返回 3,Math.round(2.5)返回 3
Math.sqrt(x) 返回 x 的平方根

RegExp 对象

正则表达式表示输入文本中的模式。RegExp对象是一个函数。它表示一个正则表达式,并包含几个方法和属性来匹配输入文本中的模式。RegExp可用作函数或构造函数。它的签名是:

RegExp(pattern, flags)

这里,pattern是输入文本中要匹配的模式。它可以是另一个RegExp对象或代表模式的字符串。flags是定义文本匹配规则的字符串。可以是空字符串,undefined,也可以是包含一个或全部字符的字符串:gim。在一个RegExp对象中重复标志是一个SyntaxError。表 4-15 包含了标志及其描述的列表。

表 4-15。

The List of Flags Used in Regular Expressions

描述
g 执行全局匹配。默认是在第一次匹配后停止匹配
i 执行不区分大小写的匹配。默认情况下,执行区分大小写的匹配
m 启用多线模式。默认为单行模式。在单行模式下,模式中的^和`
--- ---
g 执行全局匹配。默认是在第一次匹配后停止匹配
i 执行不区分大小写的匹配。默认情况下,执行区分大小写的匹配
字符分别匹配输入文本的开头和结尾。在多行模式下,它们匹配输入文本的开头和结尾以及行的开头和结尾

根据以下规则,RegExp对象可用作函数或构造函数:

  • 如果pattern是一个RegExp对象,flagsundefined,则返回pattern
  • 如果pattern是一个RegExp对象,而flags不是undefined,则生成一个SyntaxError
  • 否则,返回一个代表由patternflags指定的模式的RegExp对象

以下是创建RegExp对象的例子:

// Creates a RegExp object with a pattern to match the word

// "Java" in the input text. The g flag will perform the match

// globally and the m flag will use multiline mode.

var pattern1 = new RegExp("Java", "gm");

// Assigns pattern1 to pattern2\. The constructor returns

// pattern1 because its first argument, pattern1, is a RegExp object.

var pattern2 = new RegExp(pattern1);

// Prints true

print(pattern1 === pattern2);

您还可以使用正则表达式文本创建正则表达式。使用正则表达式文字的语法是:

/pattern/flags

以下是使用正则表达式文字创建正则表达式的示例:

var pattern1 = /Java/gm;  // The same as new RegExp("Java", "gm")

var pattern2 = /Nashorn/; // The same as new RegExp("Nashorn")

每次对正则表达式进行求值时,它都被转换成一个RegExp对象。根据这个规则,如果两个内容相同的正则表达式文字出现在同一个程序中,它们将计算为两个不同的RegExp对象,如下所示:

// pattern1 and pattern2 have the same contents, but they evaluate to // different RegExp objects

var pattern1 = /Java/gm;

var pattern2 = /Java/gm;

// Prints false in both cases

print(pattern1 === pattern2);

print(/Nashoern/ === /Nashorn/);

RegExp.prototype对象包含表 4-16 中列出的三种方法。test()方法是exec()方法的一个特例。toString()方法返回正则表达式的字符串形式。你只需要详细学习exec()的方法。

表 4-16。

The List of Methods in the RegExp.prototype Object

方法 描述
exec(string) 根据正则表达式对string执行匹配。返回一个包含匹配结果的Array对象,如果不匹配,则返回null
test(string) 调用exec(string)方法。如果exec()方法返回 not null,则返回true,否则返回false。如果您只想知道是否有匹配,请使用此方法而不是exec()方法
toString() 返回形式为/pattern/gim的字符串,其中gim是在创建RegExp对象时指定的标志

正则表达式是任何编程语言非常强大的特性。Java 支持正则表达式。如果您熟悉 Java 或 Perl 中的正则表达式,下面的讨论将很容易理解。正则表达式中的模式可能非常复杂。通常,模式中的字母和数字与输入文本完全匹配。例如,正则表达式/Java/将尝试匹配输入文本中任何位置的单词Java。下面的代码使用test()方法检查正则表达式是否与输入文本匹配:

var pattern = new RegExp("Java"); // Same as var pattern = /Java/;

var match = pattern.test("JavaScript");

print(pattern + " matches \"JavaScript\": " + match);

// Perfoms a case-sensitive match that is the default

var match2 = /java/.test("JavaScript");

print("/java/ matches \"JavaScript\": " + match2);

// Performs a case-insensitive match using the i flag

var match3 = /java/i.test("JavaScript");

print("/java/i matches \"JavaScript\": " + match3);

/Java/ matches "JavaScript": true

/java/ matches "JavaScript": false

/java/i matches "JavaScript": true

RegExp对象有四个属性,如表 4-17 所列。sourceglobalignoreCasemultiline属性是只读的、不可计数的和不可配置的,它们是在创建正则表达式时设置的。lastIndex属性是可写、不可数和不可配置的。

表 4-17。

The List of Methods in the RegExp.prototype Object

财产 描述
source 一个字符串,它是RegExp对象的模式部分。如果您使用new RegExp("Java", "gm")创建一个正则表达式,那么对象的source属性被设置为“Java”
global 如果标志g被使用,则布尔值被设置为true,否则为false
ignoreCase 如果标志i被使用,则布尔值被设置为true,否则为false
multiline 如果标志m被使用,则布尔值被设置为true,否则为false
lastIndex 它是一个整数值,指定输入文本中下一个匹配开始的位置。当RegExp对象被创建时,它被设置为零。它可以在您执行匹配时自动更改,也可以在代码中更改

exec()方法根据模式搜索输入文本,并返回nullArray对象。当没有找到匹配时,它返回null。当找到匹配时,返回的Array对象包含以下信息:

  • 数组的第一个元素是输入文本的匹配文本。数组中的后续元素(如果有)是通过模式中的捕获括号匹配的文本。(我将在本章后面讨论捕捉括号。)
  • 该数组有一个index属性,该属性被设置为匹配开始的位置。
  • 该数组有一个设置为输入文本的input属性。
  • 如果搜索是全局的(使用标志g),那么RegExp对象的lastIndex属性被设置为紧接在匹配文本之后的输入文本中的位置。如果对于全局搜索,没有找到匹配,那么RegExp对象的lastIndex属性被设置为零。对于非全局匹配,lastIndex属性不变。

注意,exec()方法从由RegExp对象的lastIndex属性定义的位置开始搜索。如果执行全局搜索,可以使用循环来查找输入文本中的所有匹配项。清单 4-25 展示了如何使用exec()方法。

清单 4-25。使用 RegExp 对象的 exec()方法进行全局匹配

// regexexec.js

var pattern = /Java/g;

var text = "Java and JavaScript are not the same";

var result;

while((result = pattern.exec(text)) !== null) {

var msg = "Matched '" + result[0] + "'" + " at " + result.index +

". Next match will begin at " + pattern.lastIndex;

print(msg);

}

print("After the search finishes, lastIndex = " + pattern.lastIndex);

Matched 'Java' at 0\. Next match will begin at 4

Matched 'Java' at 9\. Next match will begin at 13

After the search finishes, lastIndex = 0

注意,当搜索不是全局的时,不要在循环中使用exec()方法,因为对于非全局匹配,它不会改变将使循环成为无限循环的lastIndex属性。对于非全局匹配,应该只调用一次该方法。以下代码将永远运行:

var pattern = /Java/; // A non-global pattern

var text = "Java and JavaScript are not the same";

var result;

/* Do not use this loop. It will run forever because

pattern is non-global.

*/

while((result = pattern.exec(text)) !== null) {

// Code goes here

}

有些字符在特定的上下文中使用时有特殊的含义。这些字符被称为元字符,它们是(, )[]{}\^$|?*+和圆点。我将在本节的适当位置讨论这些字符的特殊含义。

在正则表达式中,一对括号内的一组字符称为字符类。正则表达式将匹配集合中的任何字符。例如,/[abc]/将匹配输入文本中的abc。您还可以使用字符类指定字符范围。范围用连字符表示。例如,[a-z]代表任何小写英文字母;[0-9]表示 0 到 9 之间的任意数字。如果你在一个字符类的开头使用字符^,它的意思是补码(意思是不补码)。例如,[^abc]表示除abc以外的任何字符。字符类[^a-z]代表除小写英文字母以外的任何字符。如果你在字符类的任何地方使用字符^,除了开头,它失去了特殊的含义,它只匹配一个^字符。例如,[abc^]将匹配abc^。字符类还支持多个范围和对这些范围的操作,如并集、交集、减法。表 4-18 显示了一些在字符类中使用范围的例子。纳斯霍恩有一些预定义的字符类,如表 4-19 所列。

表 4-19。

The List of the Predefined Character Classes

预定义的字符类 意义
。(一个点) 任何字符(可能与行终止符匹配,也可能不匹配)
\d 一个数字。同[0-9]
\D 一个非数字。同[⁰-9]
\s 空白字符。同[\t\n\x0B\f\r]。该列表包括空格、制表符、换行符、垂直制表符、换页符和回车符
\S 非空白字符。同[^\s]
\w 一个单词字符。同[a-zA-Z_0-9]。该列表包括小写字母、大写字母、下划线和十进制数字
\W 非单词字符。同[^\w]

表 4-18。

A Few Examples of Character Classes

字符类别 意义 种类
[abc] 人物ab,c 简单字符类
[^xyz] 除了xy,z之外的一个角色 补充还是否定
[a-p] 字符ap 范围
[a-cx-z] 人物ac,或xz,包括abcxyz 联盟
[0-9&&[4-8]] 两个范围的交集(45678) 交集
[a-z&&[^aeiou]] 所有小写字母减去元音。换句话说,不是元音的小写字母。也就是全部小写辅音。 减法

您还可以指定正则表达式中的字符与字符序列匹配的次数。如果想匹配所有两位数的整数,正则表达式应该是/\d\d/,与/[0-9][0-9]/相同。匹配整数的正则表达式是什么?您无法编写正则表达式来用您到目前为止获得的知识匹配一个整数。您需要能够使用正则表达式表达“一位数或更多位数”的模式。表 4-20 列出了量词及其含义。

表 4-20。

Quantifiers and Their Meanings

量词 意义
* 零次或多次
+ 一次或多次
? 一次或根本没有
{m} 恰好m
{m, } 至少m
{m, n} 至少 m 次,但不超过 n 次

注意量词必须跟在它指定数量的字符或字符类之后,这一点很重要。匹配任何整数的正则表达式应该是/\d+/,它指定:“匹配一个或多个数字”。这个匹配整数的解法对吗?不,不是。假设文本是“这是包含 10 和 120 的文本 123”。如果我们将模式/\d+/与文本匹配,它将与 123、10 和 120 匹配。下面的代码演示了这一点:

var pattern = /\d+/g;

var text = "This is text123 which contains 10 and 120";

var result;

while((result = pattern.exec(text)) !== null) {

print("Matched '" + result[0] + "' at " + result.index +

". Next match will begin at " + pattern.lastIndex);

}

Matched '123' at 12\. Next match will begin at 15

Matched '10' at 31\. Next match will begin at 33

Matched '120' at 38\. Next match will begin at 41

注意,123 不是一个整数,而是单词 text123 的一部分。如果您在文本中寻找整数,那么 text123 中的 123 肯定不是整数。您希望匹配文本中构成单词的所有整数。您需要指定只在单词边界上进行匹配,而不是在嵌入了整数的文本中。这是从结果中排除整数 123 所必需的。表 4-21 列出了在正则表达式中匹配边界的元字符。

表 4-21。

List of Boundary Matchers Inside Regular Expressions

边界匹配器 意义
^ 一行的开始
` 边界匹配器
--- ---
^ 一行的开始
一行的结尾
\b 单词边界
\B 非单词边界
\A 输入的开始
\G 上一场比赛的结束
\Z 输入的结尾,但最后一个终止符除外,如果有的话
\z 输入的结束

有了边界匹配器的知识,您可以重写前面的示例,只匹配输入文本中的整数:

var pattern = /\b\d+\b/g;

var text = "This is text123 which contains 10 and 120";

var result;

while((result = pattern.exec(text)) !== null) {

print("Matched '" + result[0] + "' at " + result.index +

". Next match will begin at " + pattern.lastIndex);

}

Matched '10' at 31\. Next match will begin at 33

Matched '120' at 38\. Next match will begin at 41

String对象有一个replace(regExp, replacement)方法,可以用来在文本中执行查找和替换操作。以下代码查找文本中出现的所有单词apple,并用单词orange替换它:

var pattern = /\bapple\b/g;

var text = "I have an apple and five pineapples.";

var replacement = "orange";

var newText = text.replace(pattern, replacement);

print("Regular Expression: " + pattern + ", Input Text: " + text);

print("Replacement Text: " + replacement + ", New Text: " + newText);

Regular Expression: /\bapple\b/g, Input Text: I have an apple and five pineapples.

Replacement Text: orange, New Text: I have an orange and five pineapples.

您可以将多个字符作为一个组来使用,从而将它们视为一个单元。通过将一个或多个字符括在括号内,可以在正则表达式中创建组。(ab)ab(z)ab(ab)(xyz)(the((is)(are)))是组的例子。正则表达式中的每个组都有一个组号。组号从 1 开始。左括号开始一个新组。您可以在正则表达式中反向引用组号。假设您想要匹配文本,该文本以ab开头,后跟xy,后跟ab。可以把正则表达式写成/abxyab/。你也可以通过组成一个包含ab的组并反向引用为/(ab)xy\1/来达到同样的结果,其中\1指的是组 1,在本例中是(ab)。注意,从RegExp对象的exec()方法返回的Array对象包含第一个元素中的匹配文本和后续元素中的所有匹配组。如果您想有一个组,但不想捕获它的结果或反向引用它,您可以将字符序列?:添加到组的开头;例如,(?:ab)xy包含一个模式为ab的组,但是您不能将其引用为\1,并且它的匹配文本不会被填充到由exec()方法返回的Array对象中。以?:开头的组称为非捕获组,不以?:开头的组称为捕获组。还可以将String对象的replace()方法中的替换文本中的抓取文本引用为$1、$2$3,以此类推,其中 1、2、3 为组号。表 4-22 列出了可用于替换文本的所有字符组合,以引用匹配文本。

表 4-22。

List of Boundary Matchers Inside Regular Expressions

特性 替换文本
`$ 特性
--- ---
$
` $$
$& 匹配的子字符串
`$`` 匹配子字符串之前的字符串部分。它是一个$后面跟着一个反引号
$' 匹配子字符串之后的字符串部分。它是一个$后跟一个单引号
$n 指组n的匹配文本,其中n为 1 到 99 之间的整数。如果组nundefined,则使用空字符串。如果 n 大于组的数量,则结果为$n
` 匹配的子字符串
` 特性
--- ---
` | 匹配子字符串之前的字符串部分。它是一个$后面跟着一个反引号
`` | 匹配子字符串之后的字符串部分。它是一个$后跟一个单引号
$n | 指组n的匹配文本,其中n为 1 到 99 之间的整数。如果组nundefined,则使用空字符串。如果 n 大于组的数量,则结果为$n

在替换文本中使用分组和反向引用,您可以将 10 位数的电话号码nnnnnnnnnn格式化为(nnn) nnn-nnnn,如清单 4-26 所示。

清单 4-26。格式化 10 位数的电话号码

// tendigitsphonesformatter.js

var pattern = /\b(\d{3})(\d{3})(\d{4})\b/g;

var text = "3342449999, 2229822, and 6152534734";

var replacement = "($1) $2-$3";

var formattedPhones = text.replace(pattern, replacement);

print("Phones: " + text);

print("Formatted Phones: " + formattedPhones);

Phones: 3342449999, 2229822, and 6152534734

Formatted Phones: (334) 244-9999, 2229822, and (615) 253-4734

在本例中,7 位数的电话号码没有格式化。假设你想把一个 7 位数的电话格式化为nnn-nnnn。您可以使用替换函数作为String对象的replace()方法的第二个参数来实现这一点。对于每个匹配项,使用以下参数调用替换函数:

  • 参数的数量是正则表达式中的组数加上 3
  • 第一个参数是匹配的文本
  • 第二个和后续参数是从组 1 开始的组的匹配文本(如果有)
  • 最后一组的匹配文本后跟输入文本中找到匹配的位置
  • 最后一个参数是输入文本本身

替换函数的返回值替换了由replace()方法返回的结果中的匹配文本。清单 4-27 显示了如何使用替换函数来格式化 7 位数和 10 位数的电话号码。注意,第一组三位数在模式中是可选的。如果一个组在匹配的文本中不匹配,反向引用该组将返回undefined。该逻辑用于phoneFormatter()功能。如果名为group1的参数是undefined,则意味着匹配了一个 7 位数的电话号码;否则,匹配的是一个 10 位数的电话号码。

清单 4-27。使用替换函数格式化 7 位数和 10 位数的电话号码

// phoneformatter.js

// Formats 10-digit and 7-digit phone numbers

function phoneFormatter(macthedText, group1, group2, group3, startIndex, inputText) {

if (group1 === undefined) {

// Matched a 7-digit phone number

return group2 + "-" + group3;

}

else {

// Matched a 10-digit phone number

return "(" + group1 + ") " + group2 + "-" + group3;

}

}

// Make the first group of 3-digits optional using the ? metacharacter

var pattern = /\b(\d{3})?(\d{3})(\d{4})\b/g;

var text = "3342449999, 2229822, and 6152534734";

// Use the phoneFormatter() function as the replacement in the replace() // method

var formattedPhones = text.replace(pattern, phoneFormatter);

print("Phones: " + text);

print("Formatted Phones: " + formattedPhones);

Phones: 3342449999, 2229822, and 6152534734

Formatted Phones: (334) 244-9999, 222-9822, and (615) 253-4734

了解脚本位置

Nashorn 提供了三个全局对象:

  • __FILE__
  • __DIR__
  • __LINE__

请注意这些属性名称前后的两个下划线。它们包含脚本文件名、脚本文件的目录名以及读取__LINE__属性的脚本的行号。如果属性__FILE____DIR__不可用,例如从标准输入中读取脚本时,则可以将其报告为null。有时,您会得到一个不是您的脚本的真实文件名的文件名。您可以使用Reader在 Java 程序的文件中运行您的脚本,在这种情况下,文件名将被报告为<eval>。清单 4-28 包含打印这些属性的代码。代码存储在一个名为scriptdetails.js的文件中。

清单 4-28。scriptdetails.js 文件的内容

// scriptdetails.js

// Print the location details of the following statement

print("File Name =", __FILE__, ", Directory =",__DIR__, ", Line # =" ,__LINE__);

以下命令使用打印脚本详细信息的jjs命令在命令行上运行scriptdetails.js文件:

c:\>jjs c:\kishori\scriptdetails.js

File Name = c:\kishori\scriptdetails.js, Directory = c:\kishori\, Line # = 4

Tip

通常,__FILE____DIR____LINE__全局属性用于调试目的。您还可以使用__DIR__属性从相对于当前运行的脚本文件的位置加载脚本。

内置全局函数

Nashorn 定义了内置的全局函数。我已经讨论过一个这样的函数,名为eval(),用于评估存储在字符串中的脚本。在这一节中,我将讨论一些更多的内置全局函数。

parseInt()函数

函数的作用是从字符串中解析出一个整数。它有以下签名:

parseInt(string, radix)

该函数通过解析指定的string返回一个整数。如果指定了radix,它被认为是string中整数的基数。string中的前导空格被忽略。如果没有指定radixundefined或 0,radix被假定为 10,除非string以 0x 或 0X(忽略前导空格)开始,在这种情况下radix被假定为 16。如果radix小于 2 或大于 36,函数返回NaN

parseInt()函数只解析string的前导部分,该部分可以是用指定的radix表示的整数中的有效数字。当遇到无效字符时,它停止解析并返回到目前为止解析的整数值。如果没有前导字符可以解析成整数,则返回NaN。请注意,当string包含无效字符时,您不会得到任何错误。该函数总是返回值。

Tip

Nashorn 中的parseInt()函数的工作方式类似于 Java 中的Integer.parseInt()方法,只是前者非常宽松,不会抛出异常。

以下代码片段显示了使用不同参数调用parseInt()函数的输出:

printf("parseInt('%s') = %d", "1969", parseInt('1969'));

printf("parseInt('%s') = %d", "  1969", parseInt('  1969'));

printf("parseInt('%s') = %d", "  1969 Hello", parseInt('  1969 Hello'));

printf("parseInt('%s') = %s", "Hello1969", parseInt('Hello1969'));

printf("parseInt('%s') = %d", "0x1969", parseInt('0x1969'));

printf("parseInt('%s', 16) = %d", "0x1969", parseInt('0x1969', 16));

printf("parseInt('%s', 2) = %d", "1001001", parseInt('1001001', 2));

printf("parseInt('%s') = %d", "-1969", parseInt('-1969'));

printf("parseInt('%s') = %s", "-  1969", parseInt('-  1969'));

printf("parseInt('%s') = %s", "xyz", parseInt('xyz'));

parseInt('1969') = 1969

parseInt('  1969') = 1969

parseInt('  1969 Hello') = 1969

parseInt('Hello1969') = NaN

parseInt('0x1969') = 6505

parseInt('0x1969', 16) = 6505

parseInt('1001001', 2) = 73

parseInt('-1969') = -1969

parseInt('-  1969') = NaN

parseInt('xyz') = NaN

函数的作用是

parseFloat()函数用于从字符串中解析浮点数。它有以下签名:

parseFloat(string)

parseFloat()函数的工作方式与parseInt()函数相同,只是前者解析string得到一个浮点数。如果string不包含数字,则返回NaN。以下代码显示了使用不同参数调用parseFloat()函数的输出:

printf("parseFloat('%s') = %f", "1969.0919", parseFloat('1969.0919'));

printf("parseFloat('%s') = %f", "  1969.0919", parseFloat('  1969.0919'));

printf("parseFloat('%s') = %f", "-1969.0919", parseFloat('-1969.0919'));

printf("parseFloat('%s') = %f", "-1.9690919e3", parseFloat('1.9690919e3'));

printf("parseFloat('%s') = %f", "1969Hello", parseFloat('1969Hello'));

printf("parseFloat('%s') = %f", "Hello", parseFloat('Hello'));

parseFloat('1969.0919') = 1969.091900

parseFloat('  1969.0919') = 1969.091900

parseFloat('-1969.0919') = -1969.091900

parseFloat('-1.9690919e3') = 1969.091900

parseFloat('1969Hello') = 1969.000000

parseFloat('Hello') = NaN

isNaN()函数

isNaN()函数具有以下签名:

isNaN(value)

如果value转换为数字时产生NaN,则返回true;也就是说,如果Number(value)返回NaN,则返回true。如果该值代表一个数字,isNaN()返回false。注意isNaN()不仅检查value是否为NaN;它检查value是否是一个数字。NaN不被认为是数字,所以isNaN(NaN)返回true。如果您有兴趣检查value在哪里是NaN,请使用表达式value !== value. NaN是唯一不等于自身的值。所以,当且仅当valueNaN时,value !== value将返回true。以下代码显示了如何使用isNaN()功能:

printf("isNaN(%s) = %b", NaN, isNaN(NaN));

printf("isNaN('%s') = %b", "123", isNaN('123'));

printf("isNaN('%s') = %b", "Hello", isNaN('Hello'));

printf("isNaN('%s') = %b", "97Hello", isNaN('97Hello'));

printf("isNaN('%s') = %b", "Infinity", isNaN('Infinity'));

printf("isNaN(%s) = %b", "1.89e23", isNaN(1.89e23));

var value = NaN;

if (value !== value) {

print("value is NaN")

}

else {

print("value is not NaN")

}

isNaN(NaN) = true

isNaN('123') = false

isNaN('Hello') = true

isNaN('97Hello') = true

isNaN('Infinity') = false

isNaN(1.89e23) = false

value is NaN

isFinite()函数

isFinite()函数具有以下签名:

isFinite(value)

如果value是一个有限的数字或者它可以被转换成一个有限的数值,那么它返回true。否则,它返回false。换句话说,如果Number(value)返回NaN+Infinity–Infinity,则isFinite(value)函数返回false;否则返回true。以下代码显示了如何使用isFinite()功能:

printf("isFinite(%s) = %b", NaN, isFinite(NaN));

printf("isFinite(%s) = %b", Infinity, isFinite(Infinity));

printf("isFinite(%s) = %b", -Infinity, isFinite(-Infinity));

printf("isFinite(%s) = %b", 1089, isFinite(1089));

printf("isFinite('%s') = %b", "1089", isFinite('1089'));

printf("isFinite('%s') = %b", "Hello", isFinite('Hello'));

printf("isFinite(%s) = %b", "true", isFinite(true));

printf("isFinite(%s) = %b", "new Object()", isFinite(new Object()));

printf("isFinite(%s) = %b", "new Object(7889)", isFinite(new Object(7889)));

isFinite(NaN) = false

isFinite(Infinity) = false

isFinite(-Infinity) = false

isFinite(1089) = true

isFinite('1089') = true

isFinite('Hello') = false

isFinite(true) = true

isFinite(new Object()) = false

isFinite(new Object(7889)) = true

decodeURI()函数

decodeURI()函数具有以下签名:

decodeURI(encodedURI)

在用转义序列代表的字符替换转义序列后,decodeURI()函数返回encodedURI。下面的代码对包含%20 作为转义序列的 URI 进行解码,该转义序列被转换为空格:

var encodedUri = "http://www.jdojo.com/hi%20there

var decodedUri = decodeURI(encodedUri);

print("Encoded URI:", encodedUri);

print("Decoded URI:", decodedUri);

Encoded URI:http://www.jdojo.com/hi%20there

Decoded URI:http://www.jdojo.com/hi

decodeURIComponent()函数

decodeURIComponent()函数具有以下签名:

decodeURIComponent(encodedURIComponent)

decodeURIComponent()函数用于将 URI 组件中的转义序列(URI 查询部分中的值)替换为相应的字符。例如,%26 需要在&符号的值中用作转义序列。这个函数将把%26 转换成一个&符号。以下代码显示了如何使用该函数:

var encodedUriComponent = "Ken%26Donna";

var decodedUriComponent = decodeURIComponent(encodedUriComponent);

print("Encoded URI Component:", encodedUriComponent);

print("Decoded URI Component:", decodedUriComponent);

Encoded URI Component: Ken%26Donna

Decoded URI Component: Ken&Donna

encodeURI()函数

encodeURI()函数具有以下签名:

encodeURI(uri)

在用转义序列替换某些字符后,它返回uri。下面的代码对包含空格的 URI 进行编码。空格字符编码为%20。

var uri = "http://www.jdojo.com/hi

var encodedUri = encodeURI(uri);

print("URI:", uri);

print("Encoded URI:", encodedUri);

URI:http://www.jdojo.com/hi

Encoded URI:http://www.jdojo.com/hi%20there

encodeURIComponent()函数

encodeURIComponent()函数具有以下签名:

encodeURIComponent(uriComponent)

它用于将 URI 组件中的某些字符(URI 查询部分中的值)替换为转义序列。例如,URI 组件中的&符号被转义为%26。以下代码显示了如何使用该函数:

var uriComponent = "Ken&Donna";

var encodedUriComponent = encodeURIComponent(uriComponent);

print("URI Component:", uriComponent);

print("Encoded URI Component:", encodedUriComponent);

URI Component: Ken&Donna

Encoded URI Component: Ken%26Donna

load()和 loadWithNewGlobal 函数

load()loadWIthNewGlobal()函数用于加载和评估存储在文件(本地或远程)和脚本对象中的脚本。他们的签名是:

  • load(scriptPath)
  • load(scriptObject)
  • loadWIthNewGlobal(scriptPath, args1, arg2, ...)
  • loadWIthNewGlobal(scriptObject, arg1, arg2,...)

这两个函数都加载和评估脚本。load()函数在同一个全局范围内加载脚本。全局范围中的现有名称和加载的脚本中的名称可能会冲突,加载的脚本可能会覆盖现有的全局值。

loadWithNewGlobal()函数在一个新的全局作用域中加载脚本,加载的脚本中的名字不会与加载脚本的全局作用域中已经存在的名字冲突。该函数允许您将参数传递给新的全局范围。您可以在脚本路径或脚本对象后指定参数。这样,现有的全局作用域可以将信息传递给新的全局作用域。可以使用arguments全局对象在加载的脚本中访问传递的参数。当加载的脚本在新的全局范围内修改内置的全局对象时,如StringObject等,这些修改在调用者的全局范围内是不可用的。

Tip

在使用新的全局作用域的新线程中执行脚本时,loadWithNewGlobal()函数非常有用,因此消除了线程覆盖现有全局变量的可能性。这两个函数都返回一个值,该值是加载的脚本中最后计算的表达式的值。

您已经使用了load()函数来加载本章中的脚本。您可以指定脚本的本地路径(绝对或相对)或 URL:

// Load a script from a local path from the current directory

load("Point.js");

// Load a script from a local path using an absolute path on Windows

load("C:\\scripts\\Point.js");

// Load a script from Internet

load("http://jdojo.com/scripts/Point.js

load()loadWithNewGlobal()函数还支持 n ashornfx等伪 URL 方案。nashorn方案用于加载 Nashorn 脚本;例如,load("nashorn:mozilla_compat.js")加载内置的 Mozilla 兼容性脚本,以便您可以在 Nashorn 中使用 Mozilla 功能。fx方案用于加载 JavaFX 相关脚本;例如,load("fx:controls.js")加载内置的 JavaFX 脚本,该脚本将所有 JavaFX 控件类名导入到 Nashorn 代码中,因此您可以使用它们的简单名称。

load()loadWithNewGlobal()方法也可以在一个名为 script 对象的对象中加载和评估脚本。一个脚本对象应该包含两个名为namescript的属性。name属性为脚本指定一个名称,在抛出错误时用作脚本名。你可以给你的脚本起任何你想要的名字。script属性包含要加载和评估的脚本。这两个属性都是必需的。下面的代码创建一个脚本对象,该对象在标准输出中打印当前日期,加载并使用load()函数评估脚本。当脚本打印当前日期和时间时,您可能会得到不同的输出:

// Create a script object

var scriptObject = {

name: "myscript",

script: "var today = new Date; print('Today is', today);"

};

// Load and evaluate the script object

load(scriptObject);

Today is Wed Oct 08 2014 11:56:50 GMT-0500 (CDT)

让我们看一些例子来看看load()loadWithNewGlobal()函数之间的区别。我在示例中使用了一个 script 对象来保持整个代码的简短。您也可以从文件或 URL 加载脚本。

下面的代码声明了一个名为x的全局变量,并给它赋值 100。它加载了一个脚本对象,该对象也声明了一个同名的全局变量x,并给它赋值 200。当使用load()函数加载脚本对象时,它会覆盖现有全局变量x的值,如输出所示:

// Declare and initialize a glocal variable named x

var x = 100;

print("Before loading the script object: x =", x);  // x is 100

// Create a script object that declares a global variable x

var scriptObject = {name: "myscript", script: "var x = 200;"};

// Load and evaluate the script object

load(scriptObject);

print("After loading the script object: x =", x); // x is 200

Before loading the script object: x = 100

After loading the script object: x = 200

下面的代码与前面的代码相同,只是它使用了loadWithNewGlobal()函数来加载相同的脚本对象。因为loadWithNewGlobal()函数创建了一个新的全局范围,它的全局变量 x 并没有影响现有的全局范围。输出确认现有的全局变量 x 保持不变:

// Declare and initialize a glocal variable named x

var x = 100;

print("Before loading the script object: x =", x);  // x is 100

// Create a script object that declares a global variable x

var scriptObject = {name: "myscript", script: "var x = 200;"};

// Load and evaluate the script object

loadWithNewGlobal(scriptObject);

print("After loading the script object: x =", x); // x is 200

Before loading the script object: x = 100

After loading the script object: x = 100

下面的代码向您展示了如何将参数传递给loadWithNewGlobal()函数。代码只是打印传递的参数:

// Create a script object prints the passed arguments

var scriptObject = {

name: "myscript",

script: "for each(var arg in arguments) {print(arg)}"

};

// Load and evaluate the script object passing three argumsnts

loadWithNewGlobal(scriptObject, 10, 20, 30);

10

20

30

您不仅限于将原始值传递给新的全局范围。也可以传递函数。您可以使用函数在两个全局作用域之间进行通信。下面的代码将一个回调函数传递给新的全局范围。注意,当从新的全局作用域调用传递的函数时,该函数仍然使用在其创建者全局作用域中定义的变量x,而不是新的全局作用域:

var x = 100;

// Create a script object that prints the passed arguments

var scriptObject = {

name: "myscript",

script: "var x = 200; print('Inside new global. x =', x); " +                 "arguments[0]();"

};

// Load and evaluate the script object passing a function object

loadWithNewGlobal(scriptObject, function () {

print("Called back from new global. x = " + x);

});

Inside new global. x = 200

Called back from new global. x = 100

摘要

Nashorn 是 JVM 上 ECMAScript 5.1 规范的运行时实现。ECMAScript 定义了自己的语法和构造,用于声明变量、编写语句和运算符、创建和使用对象和集合、迭代数据集合等等。Nashorn 100%符合 ECMAScript 5.1 规范,因此在使用 Nashorn 时,您需要学习一套新的语言语法。

Nashorn 以两种模式运行:严格模式和非严格模式。通常,在严格模式下不允许使用容易出错的功能。您可以在脚本或函数的开头使用"use strict"指令,以严格模式执行脚本或函数。

Nashorn 语法支持两种类型的注释:单行注释和多行注释。它们与 Java 中的单行和多行注释具有相同的语法。单行注释从//开始,一直延伸到行尾。多行评论以/*开头,以*/结尾;它们可以延伸到多条线。

Nashorn 定义了几个关键字和保留字。您不能将它们用作命名变量和函数的标识符。关键字var用于声明一个变量。关键字function用于声明一个函数。

Nashorn 定义了两种数据类型:基本类型和对象类型。Undefined、Null、Number、Boolean、String 是原始数据类型;其他的都是对象类型。Nashorn 中的函数也是对象。如果需要,原始值会自动转换为对象。

Nashorn 定义了几个运算符和语句,让您可以编写脚本。大部分和 Java 里的一样。Nashorn 和 Java 最大的区别之一是 Nashorn 处理布尔表达式的方式。Nashorn 处理可转换为布尔值truefalse的真值和假值。这使得在 Nashorn 中需要布尔值的地方使用任何值成为可能。

函数是一个参数化的、命名的代码块,它只定义一次并按需执行。函数是一级对象。还可以将函数定义为函数表达式,或者使用包含函数代码的字符串。一个函数也可以作为带有new操作符的构造函数来创建新的对象。当一个函数作为一个值赋给一个对象的属性时,这个函数就称为方法。

Nashorn 中的对象只是一个地图,它是命名属性的集合。对象的属性可以具有简单数据值、对象或函数的值。Nashorn 支持三种创建新对象的方法:使用对象文字、使用构造函数和使用Object.create()方法。对象的属性也可以具有控制如何使用属性的属性。例如,将属性的writable属性设置为false会使属性变为只读。您可以使用点符号或括号符号来访问对象的属性。为对象的不存在的属性赋值会创建一个名为的新属性,并为其赋值。Nashorn 支持基于原型的对象继承。

Nashorn 包含一个名为JSON的内置对象,它分别支持将 Nashorn 对象字符串化和解析为一个字符串,以及将一个字符串解析为一个 Nashorn 对象。

在 Nashorn 中,变量和函数声明没有块范围。也就是说,块内的声明在块外也是可见的。这被称为声明提升。您可以将声明提升视为所有声明都是在声明范围的顶部进行的。

Nashorn 包含几个内置的全局对象和函数。ObjectFunctionStringNumberBoolean都是内置对象的例子。parseInt()parseFloat()load()loadWithNewGlobal()是全局函数的例子。

正则表达式代表文本中的模式。Nashorn 包含一个RegExp内置对象,它的实例表示正则表达式。您也可以使用正则表达式文字来表示RegExp对象。正则表达式文字用正斜杠括起来,并且可以选择后跟定义高级文本匹配规则的标志。/Java/gi是一个正则表达式文字的例子,它将对文本Java执行全局的、不区分大小写的匹配。RegExp对象包含了test()exec()方法来对输入文本进行匹配。String对象的replace()match()等几个方法以正则表达式为参数,执行匹配、查找替换等操作。

五、过程和编译的脚本

在本章中,您将学习:

  • 如何从 Java 程序中调用脚本编写的过程
  • 如何在 Nashorn 脚本中实现 Java 接口
  • 如何编译 Nashorn 脚本并重复执行

在脚本中调用过程

在前面的章节中,您已经看到了如何使用ScriptEngineeval()方法调用脚本。如果ScriptEngine支持过程调用,也可以直接从 Java 程序中调用用脚本语言编写的过程。

脚本语言可以允许创建过程、函数和方法。Java 脚本 API 允许您从 Java 应用中调用这样的过程、函数和方法。在本节中,我将使用术语“过程”来表示过程、函数和方法。当讨论的上下文需要时,我将使用特定的术语。

并非所有脚本引擎都需要支持过程调用。Nashorn JavaScript 引擎支持过程调用。如果有脚本引擎支持,那么脚本引擎类的实现必须实现Invocable接口。Invocable接口包含以下四种方法:

  • <T> T getInterface(Class<T> cls)
  • <T> T getInterface(Object obj, Class<T> cls)
  • Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException
  • Object invokeMethod(Object obj, String name, Object... args) throws ScriptException, NoSuchMethodException

两个版本的getInterface()方法让你得到一个用脚本语言实现的 Java 接口的实例。我将在下一节详细讨论这些函数。invokeFunction()方法允许您调用用脚本语言编写的顶级函数。invokeMethod()方法让你调用用脚本语言编写的对象的方法。

在调用过程之前,检查脚本引擎是否实现了Invocable接口是开发人员的责任。调用过程包括四个步骤:

  • 检查脚本引擎是否支持过程调用
  • 将引擎引用转换为Invocable类型
  • 评估包含该过程源代码的脚本,以便引擎编译并缓存该脚本
  • 使用Invocable接口的invokeFunction()方法调用过程和函数;使用invokeMethod()方法来调用在脚本语言中创建的对象的方法

以下代码片段检查脚本引擎实现类是否实现了Invocable接口:

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable interface

if (engine instanceof Invocable) {

System.out.println("Invoking procedures is supported.");

}

else  {

System.out.println("Invoking procedures is not supported.");

}

第二步是将引擎引用转换为Invocable接口类型:

if (engine instanceof Invocable) {

Invocable inv = (Invocable)engine;

// More code goes here

}

第三步是评估脚本,因此脚本引擎编译并存储过程的编译形式,供以后调用。以下代码片段执行此步骤:

// Declare a function named add that adds two numbers

String script = "function add(n1, n2) { return n1 + n2; }";

// Evaluate the function. Call to eval() does not invoke the function. // It just compiles it.

engine.eval(script);

最后一步是调用该过程,如下所示:

// Invoke the add function with 30 and 40 as the function's arguments.

// It is as if you called add(30, 40) in the script.

Object result = inv.invokeFunction("add", 30, 40);

invokeFunction()的第一个参数是过程的名称。第二个参数是 varargs,用于指定过程的参数。invokeFunction()方法返回过程返回的值。

清单 5-1 显示了如何调用一个函数。它调用用 Nashorn JavaScript 编写的函数。它在名为factorial.jsavg.js的文件中加载脚本。这些文件包含名为factorial()avg()的函数的 Nashorn 代码。稍后,程序使用Invocable接口的invokeFunction()调用这些函数。

清单 5-1。调用用 Nashorn JavaScript 编写的函数

// InvokeFunction.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeFunction {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable // interface

if (!(engine instanceof Invocable)) {

System.out.println(   "Invoking procedures is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

try {

String scriptPath1 = "factorial.js";

String scriptPath2 = "avg.js";

// Evaluate the scripts first, so the                         // factorial() and avg() functions are

// compiled and are available to be invoked

engine.eval("load('" + scriptPath1 + "');");

engine.eval("load('" + scriptPath2 + "');");

// Invoke the add function twice

Object result1 = inv.invokeFunction("factorial", 10);

System.out.println("factorial(10) = " + result1);

Object result2 = inv.invokeFunction("avg", 10, 20, 30);

System.out.println("avg(10, 20, 30) = " + result2);

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

factorial(10) = 3628800.0

avg(10, 20, 30) = 20.0

面向对象或基于对象的脚本语言可以让您定义对象及其方法。您可以使用Invocable接口的invokeMethod(Object obj, String name, Object... args)方法调用这些对象的方法。第一个参数是对象的引用,第二个参数是要在对象上调用的方法的名称,第三个参数是 varargs 参数,用于将参数传递给被调用的方法。

清单 5-2 包含一个 Nashorn 脚本,它创建了一个名为calculator的对象,并添加了四个方法来对两个数进行加、减、乘、除。注意,我使用了 Nashorn 语法扩展来定义函数表达式,其中没有指定大括号和return语句。

清单 5-2。在 Nashorn 脚本中创建的计算器对象

// calculator.js

// Create an object

var calculator = new Object();

// Add four methods to the prototype to the calculator object

calculator.add = function (n1, n2) n1 + n2;

calculator.subtract = function (n1, n2) n1 - n2;

calculator.multiply = function (n1, n2) n1 * n2;

calculator.divide = function (n1, n2) n1 / n2;

清单 5-3 展示了在 Nashorn 中创建的calculator对象上的方法调用。注意,该对象是在 Nashorn 脚本中创建的。要从 Java 调用对象的方法,需要通过脚本引擎获取对象的引用。程序评估calculator.js文件中创建calculator对象的脚本,并将其引用存储在名为calculator的变量中。engine.get("calculator")方法返回对 Java 代码的calculator对象的引用。

清单 5-3。在 Nashorn 中创建的对象上调用方法

// InvokeMethod.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeMethod {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements the Invocable                 // interface

if (!(engine instanceof Invocable)) {

System.out.println(                           "Invoking methods is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

try {

// Declare a global object with an add() method

String scriptPath = "calculator.js";

// Evaluate the script first

engine.eval("load('" + scriptPath + "')");

// Get the calculator object reference that was // created in the script

Object calculator = engine.get("calculator");

// Invoke the methods on the calculator object

int x = 30;

int y = 40;

Object addResult = inv.invokeMethod(calculator, "add", x, y);

Object subResult = inv.invokeMethod(calculator, "subtract", x, y);

Object mulResult = inv.invokeMethod(calculator, "multiply", x, y);

Object divResult = inv.invokeMethod(calculator, "divide", x, y);

System.out.printf("calculator.add(%d, %d) = %s%n", x, y, addResult);

System.out.printf("calculator.subtract(%d, %d) = %s%n", x, y, subResult);

System.out.printf("calculator.multiply(%d, %d) = %s%n", x, y, mulResult);

System.out.printf("calculator.divide(%d, %d) = %s%n", x, y, divResult);

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

calculator.add(30, 40) = 70

calculator.subtract(30, 40) = -10.0

calculator.multiply(30, 40) = 1200.0

calculator.divide(30, 40) = 0.75

Tip

使用Invocable界面重复执行程序、函数和方法。具有过程、函数和方法的脚本评估将中间代码存储在引擎中,从而在重复执行时获得性能增益。

在脚本中实现 Java 接口

Java 脚本 API 允许您用脚本语言实现 Java 接口。用脚本语言实现 Java 接口的优点是,您可以用 Java 代码使用接口的实例,就好像接口是用 Java 实现的一样。您可以将接口的实例作为参数传递给 Java 方法。Java 接口的方法可以使用对象的顶级过程或方法在脚本中实现。

Invocable接口的getInterface()方法用于获取在脚本中实现的 Java 接口的实例。该方法有两个版本:

  • <T> T getInterface(Class<T> cls)
  • <T> T getInterface(Object obj, Class<T> cls)

第一个版本用于获取 Java 接口的实例,该接口的方法在脚本中作为顶级过程实现。接口类型作为参数传递给该方法。假设你有一个Calculator接口,如清单 5-4 所示,它包含四个方法,分别叫做add()subtract()multiply()divide()

清单 5-4。计算器界面

// Calculator.java

package com.jdojo.script;

public interface Calculator {

double add (double n1, double n2);

double subtract (double n1, double n2);

double multiply (double n1, double n2);

double divide (double n1, double n2);

}

考虑用 Nashorn 编写的顶级函数,如清单 5-5 所示。该脚本包含四个函数,对应于Calculator接口中的函数。

清单 5-5。calculatorasfunctions.js 文件的内容

// calculatorasfunctions.js

function add(n1, n2) {

n1 + n2;

}

function subtract(n1, n2) {

n1 - n2;

}

function multiply(n1, n2) {

n1 * n2;

}

function divide(n1, n2) {

n1 / n2;

}

这两个函数为Calculator接口的四个方法提供了实现。JavaScript 引擎编译完函数后,您可以获得一个Calculator接口的实例,如下所示:

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

// Get the reference of the Calculator interface

Calculator calc = inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface implementation not found.");

}

else {

// Use calc to call the methods of the Calculator interface

}

您可以添加两个数字,如下所示:

int sum = calc.add(15, 10);

清单 5-6 展示了如何在 Nashorn 中使用顶级过程实现一个 Java 接口。请查阅脚本语言(除 Nashorn 之外)的文档,以了解它如何支持此功能。

清单 5-6。使用脚本中的顶级函数实现 Java 接口

// UsingInterfaces.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class UsingInterfaces {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the script engine implements Invocable // interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in script" +

"is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

// Create the script for add() and subtract() functions

String scriptPath  = "calculatorasfunctions.js";

try {

// Compile the script that will be stored in the // engine

engine.eval("load('" + scriptPath + "')");

// Get the interface implementation

Calculator calc =          inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

double x = 15.0;

double y = 10.0;

double addResult = calc.add(x, y);

double subResult = calc.subtract(x, y);

double mulResult = calc.multiply(x, y);

double divResult = calc.divide(x, y);

System.out.printf(                           "calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

System.out.printf(                           "calc.subtract(%.2f, %.2f) = %.2f%n", x, y, subResult);

System.out.printf(                           "calc.multiply(%.2f, %.2f) = %.2f%n", x, y, mulResult);

System.out.printf(                           "calc.divide(%.2f, %.2f) = %.2f%n", x, y, divResult);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

calc.add(15.00, 10.00) = 25.00

calcr.subtract(15.00, 10.00) = 5.00

calcr.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

Nashorn 引擎是如何找到Calculator接口的实现的?当您调用InvocablegetInterface(Class<T> cls)时,引擎会在指定的类中寻找具有匹配名称的编译函数作为抽象方法。在我们的例子中,引擎在引擎中寻找名为addsubtractmultiplydivide的编译函数。注意,需要调用引擎的eval()方法来编译calculatorasfunctions.js文件中的函数。Nashorn 引擎与引擎中 Java 接口方法和脚本函数中的形参数量不匹配。

第二个版本的getInterface()方法用于获得一个 Java 接口的实例,该接口的方法被实现为一个对象的实例方法。它的第一个参数是用脚本语言创建的对象的引用。对象的实例方法实现作为第二个参数传入的接口类型。清单 5-2 中的代码创建了一个名为calculator的对象,它的实例方法实现了Calculator接口。您将把calculator对象的方法作为 Java 中Calculator接口的实现。

当脚本对象的实例方法实现 Java 接口的方法时,您需要执行一个额外的步骤。在获取接口的实例之前,需要获取脚本对象的引用,如下所示:

// Load the calculator object in the engine

engine.load('calculator.js');

// Get the reference of the global script object calculator

Object calc = engine.get("calculator");

// Get the implementation of the Calculator interface

Calculator calculator = inv.getInterface(calc, Calculator.class);

清单 5-7 展示了如何使用 Nashorn 将 Java 接口的方法实现为对象的实例方法。

清单 5-7。将 Java 接口的方法实现为脚本中对象的实例方法

// ScriptObjectImplInterface.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ScriptObjectImplInterface {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

// Make sure the engine implements the Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in " +

"script is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

String scriptPath  = "calculator.js";

try {

// Compile and store the script in the engine

engine.eval("load('" + scriptPath + "')");

// Get the reference of the global script object calc

Object scriptCalc = engine.get("calculator");

// Get the implementation of the Calculator interface

Calculator calc = inv.getInterface(scriptCalc, Calculator.class);

if (calc == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

double x = 15.0;

double y = 10.0;

double addResult = calc.add(x, y);

double subResult = calc.subtract(x, y);

double mulResult = calc.multiply(x, y);

double divResult = calc.divide(x, y);

System.out.printf("calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

System.out.printf("calc.subtract(%.2f, %.2f) = %.2f%n", x, y, subResult);

System.out.printf("calc.multiply(%.2f, %.2f) = %.2f%n", x, y, mulResult);

System.out.printf("calc.divide(%.2f, %.2f) = %.2f%n", x, y, divResult);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

calc.add(15.00, 10.00) = 25.00

calcr.subtract(15.00, 10.00) = 5.00

calcr.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

使用编译的脚本

脚本引擎可以允许编译脚本并重复执行它。执行编译后的脚本可以提高应用的性能。脚本引擎可以以 Java 类、Java 类文件的形式或特定于语言的形式编译和存储脚本。

并非所有脚本引擎都需要支持脚本编译。支持脚本编译的脚本引擎必须实现Compilable接口。Nashorn 引擎支持脚本编译。以下代码片段检查脚本引擎是否实现了Compilable接口:

// Get the script engine reference

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("YOUR_ENGINE_NAME");

if (engine instanceof Compilable) {

System.out.println("Script compilation is supported.");

}

else {

System.out.println("Script compilation is not supported.");

}

一旦知道脚本引擎实现了Compilable接口,就可以将其引用转换为Compilable类型,如下所示:

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

Compilable接口包含两个方法:

  • CompiledScript compile(String script) throws ScriptException
  • CompiledScript compile(Reader script) throws ScriptException

该方法的两个版本仅在脚本源的类型上有所不同。第一个版本接受脚本作为String,第二个版本接受脚本作为Reader

compile()方法返回一个CompiledScript类的对象。CompiledScript是一个抽象类。脚本引擎的提供者提供了这个类的具体实现。一个CompiledScript与创建它的ScriptEngine相关联。CompiledScript类的getEngine()方法返回与其关联的ScriptEngine的引用。

要执行编译后的脚本,您需要调用CompiledScript类的以下eval()方法之一:

  • Object eval() throws ScriptException
  • Object eval(Bindings bindings) throws ScriptException
  • Object eval(ScriptContext context) throws ScriptException

没有任何参数的eval()方法使用脚本引擎的默认脚本上下文来执行编译后的脚本。当你向另外两个版本传递一个Bindings或一个ScriptContext时,它们的工作方式与ScriptEngine接口的eval()方法相同。

Tip

当您使用CompiledScript类的eval()方法评估脚本时,在已编译脚本的执行过程中对引擎状态所做的更改可能在引擎随后执行脚本时可见。

清单 5-8 显示了如何编译并执行一个脚本。它使用不同的参数将相同的编译脚本执行两次。

清单 5-8。使用编译的脚本

// CompilableTest .java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.Compilable;

import javax.script.CompiledScript;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class CompilableTest {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

if (!(engine instanceof Compilable)) {

System.out.println("Script compilation not supported.");

return;

}

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

try {

// Compile a script

String script = "print(n1 + n2)";

CompiledScript cScript = comp.compile(script);

// Store n1 and n2 script variables in a Bindings

Bindings scriptParams = engine.createBindings();

scriptParams.put("n1", 2);

scriptParams.put("n2", 3);

cScript.eval(scriptParams);

// Execute the script again with different values // for n1 and n2

scriptParams.put("n1", 9);

scriptParams.put("n2", 7);

cScript.eval(scriptParams);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

5

16

摘要

Java 脚本 API 支持直接从 Java 调用用脚本语言编写的过程、函数和方法。这可以通过Invocable界面实现。如果脚本引擎支持过程调用,它会实现Invocable接口。Nashorn 引擎支持过程调用。被调用的过程可以被实现为对象的顶层函数或方法。Invocable接口的invokeFunction()方法用于调用脚本中的顶层函数。Invocable接口的invokeMethod()方法用于调用一个对象的方法。在调用顶级函数和对象之前,必须由引擎对其方法进行评估。

Java Script API 还允许您用脚本语言实现 Java 接口。Java 接口的方法可以被实现为对象的顶级函数或方法。Invocable接口的getInterface()方法用于获取 Java 接口的实现。

Java 脚本 API 还允许您编译一次脚本,将其存储在脚本引擎中,并多次执行脚本。通过Compilable接口支持。支持脚本编译的脚本引擎需要实现Compilable接口。你需要调用Compilable接口的compile()方法来编译脚本。compile()方法返回CompiledScript的实例,调用其eval()方法来执行脚本。

注意,通过脚本引擎实现InvocableCompilable接口是可选的。在调用过程和编译脚本之前,您需要检查脚本引擎是否是这些接口的实例,将引擎转换为这些类型,然后执行这些接口的方法。

六、在脚本语言中使用 Java

在本章中,您将学习:

  • 如何将 Java 类导入脚本
  • 如何创建 Java 对象并在脚本中使用它们
  • 如何调用 Java 对象的重载方法
  • 如何创建 Java 数组
  • 如何在脚本中扩展 Java 类和实现 Java 接口
  • 如何从脚本中调用对象的超类方法

脚本语言允许在脚本中使用 Java 类库。每种脚本语言都有自己的使用 Java 类的语法。讨论所有脚本语言的语法是不可能的,也超出了本书的范围。在这一章中,我将讨论在 Nashorn 中使用 Java 构造的语法。

导入 Java 类型

有四种方法可以将 Java 类型导入 Nashorn 脚本:

  • 使用Packages全局对象
  • 使用Java全局对象的type()方法
  • 使用importPackage()importClass()功能
  • with子句中使用JavaImporter

在导入 Java 类型的四种类型中,使用全局 Java 对象的type()方法的第二种类型是首选的。下面几节将详细描述在脚本中导入 Java 类型的四种方式。

使用包全局对象

Nashorn 将a ll Java 包定义为名为Packages的全局变量的属性。例如,java.langjavax.swing包可以分别称为Packages.java.langPackages.javax.swing。以下代码片段使用了 Nashorn 中的java.util.Listjavax.swing.JFrame:

// Create a List

var list1 = new Packages.java.util.ArrayList();

// Create a JFrame

var frame1 = new Packages.javax.swing.JFrame("Test");

Nashorn 将javajavaxorgcomedunet声明为全局变量,分别是Packages.javaPackages.javaxPackages.orgPackages.comPackages.eduPackages.net的别名。本书示例中的类名以前缀com开头,例如com.jdojo.script.Test。要在 JavaScript 代码中使用这个类名,可以使用Packages.com.jdojo.script.Testcom.jdojo.script.Test。但是,如果一个类名不是以这些预定义的前缀之一开头,您必须使用Packages全局变量来访问它;例如,如果您的类名是p1.Test,您需要在 JavaScript 代码中使用Packages.p1.Test来访问它。以下代码片段使用Packages.javaPackages.javaxjavajavax别名:

// Create a List

var list = new java.util.ArrayList();

// Create a JFrame

var frame = new javax.swing.JFrame("Test");

使用 Java 全局对象

Java 7 中的 Rhino JavaScript 也支持将包作为Packages对象的属性来访问。使用Packages对象速度较慢并且容易出错。Nashorn 定义了一个名为Java的新的全局对象,它包含许多有用的函数来处理 Java 包和类。如果您使用的是 Java 8 或更高版本,您应该更喜欢使用Java对象。对象的type()函数将一个 Java 类型导入到脚本中。您需要传递要导入的 Java 类型的完全限定名。在 Nashorn 中,以下代码片段导入了java.util.ArrayList类并创建了它的对象:

// Import java.util.ArrayList type and call it ArrayList

var ArrayList = Java.type("java.util.ArrayList");

// Create an object of the ArrayList type

var list = new ArrayList();

您也可以将两个语句合并为一个。确保将对Java.type()方法的调用添加到一对括号中;否则,该语句将生成一个错误,认为 Java.type 是一个构造函数,而您正试图使用构造函数创建一个 Nashorn 对象:

// Create an object of the java.util.ArrayList type

var list = new``(``Java.type("java.util.ArrayList")``)

在代码中,您将从Java.type()函数返回的导入类型称为ArrayList,这也是导入的类的名称。这样做是为了让下一条语句看起来像是用 Java 编写的。第二条语句的读者会知道你正在创建一个ArrayList类的对象。但是,您可以为导入的类型指定任何想要的名称。下面的代码片段导入java.util.ArrayList并将其命名为MyList:

// Import java.util.ArrayList type and call it MyList

var MyList = Java.type("java.util.ArrayList");

// Create an object of the MyList type

var list2 = new MyList();

使用 importPackage()和 importClass()函数

Rhino JavaScript 允许在脚本中使用 Java 类型的简单名称。Rhino JavaScript 有两个名为importPackage()importClass()的内置函数,分别用于从一个包中导入所有类和从一个包中导入一个类。出于兼容性的考虑,Nashorn 保留了这些功能。要在 Nashorn 中使用这些功能,您需要使用load()功能从mozilla_compat.js文件中加载兼容模块。以下代码片段改写了上一节“这些函数”中的上述逻辑:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

// Import ArrayList class from the java.util package

importClass(java.util.ArrayList);

// Import all classes from the javax.swing package

importPackage(javax.swing);

// Use simple names of classes

var list = new ArrayList();

var frame = new JFrame("Test");

JavaScript 不会自动从java.lang包中导入所有的类,因为同名的 JavaScript 类,例如StringObjectNumbe r 等等,会与java.lang包中的类名冲突。要使用来自java.lang包的类,您可以导入它或者使用PackagesJava全局对象来使用它的完全限定名。您不能从java.lang包中导入所有的类。下面的代码片段生成了一个错误,因为 JavaScript 中已经定义了String类名:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

// Will cause a conflict with String object in Nashorn

importClass(java.lang.String);

如果你想使用java.lang.String类,你需要使用它的完全限定名。以下代码片段使用内置的 JavaScript String类和java.lang.String类:

var javaStr = new java.lang.String("Hello"); // Java String class

var jsStr = new String("Hello");             // JavaScript String class

如果java.lang包中的类名与 JavaScript 顶级类名不冲突,可以使用importClass()函数导入 Java 类。例如,您可以使用下面的代码片段来使用java.lang.System类:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

importClass(java.lang.System);

var jsStr = new String("Hello");

System.out.println(jsStr);

在这段代码中,jsStr是一个 JavaScript String,它被传递给接受java.lang.String类型的System.out.println() Java 方法。在这种情况下,JavaScript 自动处理从 JavaScript 类型到 Java 类型的转换。

使用 JavaImporter 对象

在 JavaScript 中,您可以通过在with语句中使用JavaImporter对象来使用简单的类名。请参考with声明中的Chapter 4了解更多详情。JavaImporter是一个 Nashorn 函数对象,可用作函数或构造函数。它接受 Java 包和类的列表。您可以创建一个JavaImporter对象,如图所示:

// Import all classes from the java.lang package

var langPkg = new JavaImporter(Packages.java.lang);

// Import all classes from the java.lang and java.util packages and the

// JFrame class from the javax.swing package

var pkg2 = JavaImporter(java.lang, java.util, javax.swing.JFrame);

注意第一条语句中使用了new操作符。第二条语句没有使用new操作符;它使用 JavaImporter 作为函数。这两种说法做了同样的事情。

以下代码片段创建了一个JavaImporter对象,并在with语句中使用它:

// Create a Java importer for java.lang and java.util packages

var javaLangAndUtilPkg = JavaImporter(java.lang, java.util);

// Use the imported types in the with clause

with (javaLangAndUtilPkg) {

var list = new ArrayList();

list.add("one");

list.add("two");

System.out.println("Hello");

System.out.println("List is " + list);

}

Hello

List is [one, two]

创建和使用 Java 对象

使用带有构造函数的new操作符在脚本中创建新的 Java 对象。以下代码片段在 Nashorn 中创建了一个String对象:

// Create a Java String object

var JavaString = Java.type("java.lang.String");

var greeting = new JavaString("Hello");

在大多数脚本语言中,访问 Java 对象的方法和属性是相似的。一些脚本语言允许您使用属性名调用对象的 getter 和 setter 方法。Nashorn 中的以下代码创建了一个java.util.Date对象,并使用属性名和方法名来访问该对象的方法。您可能会得到不同的输出,因为代码在当前日期运行:

var LocalDate = Java.type("java.time.LocalDate");

var dt = LocalDate.now();

var year = dt.year;             // Use as a property

var month = dt.month;           // Use as a property

var date = dt.getDayOfMonth();  // Use as a method

print("Date:" + dt);

print("Year:" + year + ", Month:" + month + ", Day:" + date);

Date:2014-10-12

Year:2014, Month:OCTOBER, Day:12

在 JavaScript 中,您可以像使用属性一样使用 Java 对象的方法。当你在读取名为xxx的属性时,JavaScript 会自动调用getXxx()方法。当你设置名为xxx的属性时,会调用setXxx()方法。JavaBeans 方法约定用于查找相应的方法。例如,如果你读取一个LocalDate对象的leapYear属性,那么该对象的isLeapYear()方法将被调用,因为该属性属于boolean类型。

使用 JavaScript 时,理解不同类型的String对象很重要。一个String对象可能是一个 JavaScript String对象或者一个 Java java.lang.String对象。JavaScript 为其String对象定义了一个length属性,而 Java 为其java.lang.String类定义了一个length()方法。以下代码片段显示了创建和访问 JavaScript String和 Java java.lang.String对象的长度的不同:

// JavaScript String

var jsStr = new String("Hello JavaScript String");

print("JavaScript String: " + jsStr);

print("JavaScript String Length: " + jsStr.length);

// Java String

var javaStr = new java.lang.String("Hello Java String");

print("Java String: " + javaStr);

print("Java String Length: " + javaStr.length());

JavaScript String: Hello JavaScript String

JavaScript String Length: 23

Java String: Hello Java String

Java String Length: 17

使用重载的 Java 方法

Java 在编译时解析重载方法的方法调用。也就是说,Java 编译器确定代码运行时将调用的方法的签名。考虑清单 6-1 所示的PrintTest类的代码。您可能会在第二行得到不同的输出。

清单 6-1。在 Java 中使用重载方法

// PrintTest.java

package com.jdojo.script;

public class PrintTest {

public void print(String str) {

System.out.println("print(String): " + str);

}

public void print(Object obj) {

System.out.println("print(Object): " + obj);

}

public void print(Double num) {

System.out.println("print(Double): " + num);

}

public static void main(String[] args) {

PrintTest pt = new PrintTest();

Object[] list = new Object[]{"Hello", new Object(), 10.5};

for(Object arg : list) {

pt.print(arg);

}

}

}

print(Object): Hello

print(Object): java.lang.Object@affc70

print(Object): 10.5

当运行PrintTest类时,对print()方法的所有三个调用都调用PrintTest类的同一个版本print(Object)。当代码被编译时,Java 编译器将调用pt.print(arg)视为对带有Object类型参数的print()方法的调用(这是arg的类型),因此将该调用绑定到print(Object)方法。

在脚本语言中,变量的类型在运行时是已知的,而不是在编译时。脚本语言的解释器根据方法调用中参数的运行时类型适当地解析重载的方法调用。以下 JavaScript 代码的输出显示了对PrintTest类的print()方法的调用在运行时根据参数的类型进行解析。您可能会在第二行得到稍微不同的输出:

// JavaScript Code

// Create an object of the Java class called PrintTest

var PrintTest = Java.type("com.jdojo.script.PrintTest");

var pt = new PrintTest();

// Create a JavaScript array with three elements

var list = ["Hello", new Object(), 10.5];

// Call the overloaded method print() of the PrintTest class

// passing each object in an array at time

for each(var element in list) {

pt.print(element);

}

print(String): Hello

print(Object): jdk.nashorn.internal.scripts.JO@405818

print(Double): 10.5

JavaScript 允许您显式选择重载方法的特定版本。您可以传递要用对象引用调用的重载方法的签名。以下代码片段选择了print(Object)版本:

// JavaScript Code

var PrintTest = Java.type("com.jdojo.script.PrintTest");

var pt = new PrintTest();

pt"print(java.lang.Object)"; // Calls print(Object)

pt"print(java.lang.Double)"; // Calls print(Double)

print(Object): 10.5

print(Double): 10. 5

使用 Java 数组

在 Rhino 和 Nashorn 中,用 JavaScript 创建 Java 数组的方式是不同的。在 Rhino 中,您需要使用java.lang.reflect.Array类的newInstance()静态方法创建一个 Java 数组。Nashorn 也支持这种语法。下面的代码片段展示了如何使用 Rhino 语法创建和访问 Java 数组:

// Create a java.lang.String array of 2 elements, populate it, and print

// the elements. In Rhino, you were able to use java.lang.String as

// the first argument, but in Nashorn, you need to use

// java.lang.String.class instead.

var strArray = java.lang.reflect.Array.newInstance(java.lang.String.class, 2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

要创建原始类型数组,如intdouble等,您需要将它们的TYPE常量用于它们对应的包装类,如下所示:

// Create an int array of 2 elements, populate it, and print the elements

var intArray = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Nashorn 支持创建 Java 数组的新语法。首先,使用Java.type()方法创建适当的 Java 数组类型,然后使用熟悉的new操作符创建数组。以下代码片段显示了如何在 Nashorn 中创建两个元素的String[]:

// Get the java.lang.String[] type

var StringArray = Java.type("java.lang.String[]");

// Create a String[] array of 2 elements

var strArray = new StringArray(2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

Nashorn 支持以同样的方式创建原始类型的数组。以下代码片段在 Nashorn 中创建了两个元素的int[]:

// Get the int[] type

var IntArray = Java.type("int[]");

// Create a int[] array of 2 elements

var intArray = new IntArray(2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

我将在第七章中详细讨论如何使用 Java 和 JavaScript 数组。

扩展 Java 类实现接口

JavaScript 允许您在 JavaScript 中扩展 Java 类和实现 Java 接口。以下部分描述了实现这一点的不同方法。

使用脚本对象

您需要创建一个包含接口方法实现的脚本对象,并使用new操作符将其传递给 Java 接口的构造函数。在 Java 中,接口没有构造函数,也不能和new操作符一起使用,除非创建匿名类。然而,JavaScript 让你做到了这一点。

在第五章中,我们已经用四个抽象方法创建了Calculator接口。清单 6-2 再次显示了该接口的代码,供您参考。

清单 6-2。Java 中的计算器界面

// Calculator.java

package com.jdojo.script;

public interface Calculator {

double add (double n1, double n2);

double subtract (double n1, double n2);

double multiply (double n1, double n2);

double divide (double n1, double n2);

}

在第五章中,我们创建了一个calculator JavaScript 对象,其脚本如清单 6-3 所示。

清单 6-3。Java 中的计算器界面

// calculator.js

// Create an object

var calculator = new Object();

// Add four methods to the prototype to the calculator object

calculator.add = function (n1, n2) n1 + n2;

calculator.subtract = function (n1, n2) n1 - n2;

calculator.multiply = function (n1, n2) n1 * n2;

calculator.divide = function (n1, n2) n1 / n2;

注意 JavaScript 中的calculator对象包含了 Java Calculator接口的所有抽象方法的实现。下面的语句创建了一个Calculator接口的实现:

// Load the calculator object

load("calculator.js");

// Get the Java interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Create an instance of the com.jdojo.script.Calculator interface

// passing its constructor a calculator JavaScript object

var calc = new Calculator(calculator);

现在您可以开始使用calc对象,就好像它是Calculator接口的实现一样,如下所示:

// Use the instance of teh Calculator interface

var x = 15.0, y = 10.0;

var addResult = calc.add(x, y);

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f", x, y, addResult);

printf("calc.subtract(%.2f, %.2f) = %.2f", x, y, subResult);

printf("calc.multiply(%.2f, %.2f) = %.2f", x, y, mulResult);

printf("calc.divide(%.2f, %.2f) = %.2f", x, y, divResult);

calc.add(15.00, 10.00) = 25.00

calc.subtract(15.00, 10.00) = 5.00

calc.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

使用匿名的类语法

该方法使用的语法与在 Java 中创建匿名类的语法非常相似。以下语句实现了 Java Calculator接口,并创建了该实现的实例:

// Get the Java interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Create an instance of the com.jdojo.script.Calculator interface

// using an anonymous class-like syntax

var calc = new Calculator() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2)

};

现在你可以像以前一样使用calc对象。

使用 JavaAdapter 对象

JavaScript 允许您实现多个接口,并使用JavaAdapter类扩展一个类。然而,与 JDK 捆绑在一起的 Rhino JavaScript 实现已经覆盖了JavaAdapter的实现,它只允许你实现一个接口;它不允许你扩展一个类。JavaAdapter构造函数的第一个参数是要实现的接口,第二个参数是实现接口抽象方法的脚本对象。要在 Nashorn 中使用JavaAdapter对象,需要加载 Rhino 兼容性模块。下面的代码片段使用JavaAdapter实现了Calculator接口:

// Need to load the compatibility module in Nashorn.

// You do not need to the following load() call in Rhino.

load("nashorn:mozilla_compat.js");

// Load the script that creates the calculator JavaScript object

load("calculator.js");

var calc = new JavaAdapter(com.jdojo.script.Calculator, calculator);

现在你可以像以前一样使用calc对象。它是com.jdojo.script.Calculator接口的一个实例,其实现由在calculator.js文件的脚本中定义的calculator对象中声明的方法提供。

使用 Java.extend()方法

Nashorn 提供了一种更好的方法来扩展一个类并使用Java.extend()方法实现多个接口。在Java.extend()方法中,您最多可以传递一个类类型和多个接口类型。它返回一个组合了所有传入类型的类型。您需要使用前面讨论过的类似匿名类的语法来为新类型的抽象方法提供实现,或者覆盖被扩展类型的现有方法。下面的代码片段使用了Java.extend()方法来实现Calculator接口:

// Get the Calculator interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

// Get a type that extends the Calculator type

var CalculatorExtender = Java.extend(Calculator);

// Implement the abstract methods in CalculatorExtender

// using an anonymous class like syntax

var calc = new CalculatorExtender() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2)

};

// Use the instance of teh Calculator interface

var x = 15.0, y = 10.0;

var addResult = calc.add(x, y);

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f", x, y, addResult);

printf("calc.subtract(%.2f, %.2f) = %.2f", x, y, subResult);

printf("calc.multiply(%.2f, %.2f) = %.2f", x, y, mulResult);

printf("calc.divide(%.2f, %.2f) = %.2f", x, y, divResult);

calc.add(15.00, 10.00) = 25.00

calc.subtract(15.00, 10.00) = 5.00

calc.multiply(15.00, 10.00) = 150.00

calc.divide(15.00, 10.00) = 1.50

您可以使用Java.extend()方法来扩展具体类、抽象类和接口。下面的代码扩展了具体的类java.lang.Thread并实现了Calculator接口。新的实现覆盖了Thread类的run()方法:

// Get the Calculator interface type

var Calculator = Java.type("com.jdojo.script.Calculator");

var Thread = Java.type("java.lang.Thread");

// Get a type that extends the Calculator type

var ThreadCalcExtender = Java.extend(Thread, Calculator);

// Implement the abstract methods in CalculatorExtender

// using an anonymous class like syntax

var calcThread = new ThreadCalcExtender() {

add: (function (n1, n2) n1 + n2),

subtract: (function (n1, n2) n1 - n2),

multiply: (function (n1, n2) n1 * n2),

divide: (function (n1, n2) n1 / n2),

run: function () {

var n1 = Math.random();

var n2 = Math.random();

printf("n1 = %.2f, n2 = %.2f", n1, n2);

var addResult = this.add(n1, n2);

printf("calc.add(%.2f, %.2f) = %.2f", n1, n2, addResult);

}

};

// Start the thread

calcThread.start();

n1 = 0.61, n2 = 0.66

calc.add(0.61, 0.66) = 1.27

使用 JavaScript 函数

有时一个 Java 接口只有一个方法。在这些情况下,您可以传递一个 JavaScript 函数对象来代替接口的实现。Java 中的Runnable接口只有一个方法run()。当需要在 JavaScript 中使用Runnable接口的实例时,可以传递一个 JavaScript function 对象。下面的代码片段展示了如何创建一个Thread对象并启动它。在Thread类的构造函数中,传递的是 JavaScript 函数对象myRunFunc,而不是Runnable接口的实例:

function myRunFunc() {

print("A thread is running.");

}

// Call Thread(Runnable) constructor and pass the myRunFunc function

// object that will serve as an implementation for the run() method of

// the Runnable interface.

var thread = new java.lang.Thread(myRunFunc);

thread.start();

A thread is running.

访问超类的方法

在 Java 中,当您可以使用关键字super访问超类的方法时。当您在 JavaScript 中扩展一个类时,您也可以使用Java.super()方法访问超类的方法。该方法采用 JavaScript 中已扩展的 JavaScript 对象,并返回一个引用,该引用可用于调用超类的方法。考虑清单 6-4 所示的Person类的代码。

清单 6-4。一个人类

// Person.java

package com.jdojo.script;

public class Person {

private String firstName;

private String lastName;

public Person(String firstName, String lastName){

this.firstName = firstName;

this.lastName = lastName;

}

public String getFirstName() {

return firstName;

}

public void setFirstName(String firstName) {

this.firstName = firstName;

}

public String getLastName() {

return lastName;

}

public void setLastName(String lastName) {

this.lastName = lastName;

}

public String getFullName() {

return firstName + " " + lastName;

}

}

考虑清单 6-5 中的代码。它扩展了Person类并覆盖了getFullName()方法。

清单 6-5。使用 Java.super()方法访问超类方法

// supermethod.js

var Person = Java.type("com.jdojo.script.Person");

var PersonExtender = Java.extend(Person);

// Extend the Person class and override the getFullName() method

var john = new PersonExtender("John", "Jacobs") {

getFullName: function () {

// You can use the variable john here that is declared outside.

var _super_ = Java.super(john);

var fullName = _super_.getFullName();

return fullName.toUpperCase();

}

}

// Get john's full name using the extended class implementation

var johnFullName = john.getFullName() ;

// Get the reference of john's super

var johnSuper = Java.super(john);

// Get john's full name from the Person class

var johnSuperFullName = johnSuper.getFullName();

// Print Names

print("Extended full name:", johnFullName);

print("Super full name:", johnSuperFullName);

Extended full name: JOHN JACOBS

Super full name: John Jacobs

注意扩展的Person类的getFullName()方法引用了在函数外部声明的名为john的变量。下面的语句指定了一个对象的引用,该对象可用于调用john对象的超类的方法。

var _super_ = Java.super(john);

被覆盖的方法调用Person类的“getFullName()方法,将名称转换成大写,并返回它。代码再次获取超类引用,如下所示:

// Get the reference of john's super

var johnSuper = Java.super(john);

最后,代码打印从Person类和扩展的Person类返回的值,以显示您确实能够调用超类方法。

Tip

除了使用Java.super(obj)方法获取obj的超类对象引用并在其上调用方法,还可以使用obj.super$MethodName(args)语法调用名为obj的对象的超类的方法。例如,在示例中,您可以使用john.super$getFullName()来调用对象john上的Person类的getFullName()方法。

使用 Lambda 表达式

JavaScript 支持可以用作 lambda 表达式的匿名函数。下面是一个匿名函数,它将一个数字作为参数并返回其平方值:

function (n) {

return n * n;

}

下面是一个使用匿名函数作为 lambda 表达式在 JavaScript 中创建一个Runnable对象的例子。在Thread类的构造函数中使用了Runnable对象。

var Thread = Java.type("java.lang.Thread");

// Create a Thread using a Runnable object. The Runnable object is

// created using an anonymous function as a lambda expressions.

var thread = new Thread(function () {

print("Hello Thread");

});

// Start the thread

thread.start();

使用 lambda 表达式的 JavaScript 代码的 Java 等效代码如下:

// Create a Thread using a Runnable object. The Runnable object is

// created using a lambda expression.

Thread thread = new Thread(() -> {

System.out.println("Hello Thread");

});

// Start the thread

thread.start();

摘要

在脚本中创建 Java 对象之前,您需要将 Java 类型导入到脚本中。在脚本中有四种导入类型的方法:使用Packages全局对象,使用Java.type()方法,使用importPackage()importClass()函数,以及在with子句中使用JavaImporter。Nashorn 将javajavaxorgcomedunet声明为全局变量,它们分别是Packages.javaPackages.javaxPackages.orgPackages.comPackages.eduPackages.net的别名。因此,您可以使用这些包中任何类型的完全限定名来引用该类型。

您需要使用new操作符和 Java 类型在脚本中创建 Java 对象。使用Java.type()方法可以让您以统一的方式导入 Java 类型,包括数组类型。创建 array 对象的方法与创建任何其他类型的对象的方法相同。

大多数时候,调用重载的 Java 方法是由 Nashorn 解决的。如果希望从脚本调用重载方法的特定版本,可以使用括号标记来指定特定的方法签名。例如,名为pt的对象引用上的pt"print(java.lang.Object)" calls the print(java.lang.Object)方法将 10.5 作为参数传递给该方法。

Nashorn 允许您使用Java.extend()方法在脚本中扩展接口、抽象类和具体类。它允许您使用Java.super()方法调用对象上的超类方法。

七、集合

在本章中,您将学习:

  • 纳申中的数组是什么
  • 如何使用数组文字和Array对象在 Nashorn 中创建数组
  • 如何使用Array对象的不同方法
  • 如何使用类似数组的对象
  • 如何在 Nashorn 中创建和使用类型化数组
  • 如何在 Nashorn 中使用 Java 集合
  • 如何在 Nashorn 中创建 Java 数组
  • 如何将 Java 数组转换成 Nashorn 数组,反之亦然

纳森的数组是什么?

Nashorn 中的数组是一个专门的对象,称为Array对象,用于表示值的有序集合。一个Array对象以一种特殊的方式对待某些属性名。如果一个属性名可以转换成一个介于 0 和 2 32 -2(包括 0 和 2)之间的整数,这样的属性名称为数组索引,属性称为元素。换句话说,数组中的元素是一个特殊的属性,它的名称是数组索引。除了向数组中添加元素之外,还可以像对 Nashorn 对象一样添加任何其他属性。注意,在一个Array对象中,每个数组索引都是一个属性,但是每个属性并不是一个数组索引。例如,您可以将两个名为“0”和“name”的属性添加到数组中,其中“0”是数组索引,因为它可以转换为整数,而“name”只是一个属性。

每个Array对象都有一个名为length的属性,其值大于所有元素的索引。添加元素时,length会自动调整。如果length改变,索引大于或等于新length的元素被删除。注意,与 Java 数组不同,Nashorn 数组是可变长度数组。也就是说,Nashorn 数组不是定长数组;当添加和删除元素或者length属性改变时,它们可以扩展和收缩。

有两种类型的数组:密集数组和稀疏数组。在密集数组中,所有元素的索引都是连续的。Java 数组总是密集数组。在稀疏数组中,所有元素的索引可能不连续。Nashorn 阵列是稀疏阵列。例如,在 Nashorn 中可以有一个数组,该数组在索引 1000 处有一个元素,而在索引 0 到 999 之间没有任何元素。

与 Java 不同,Nashorn 中的数组没有类型。数组中的元素可以是混合类型,一个元素可以是数字,另一个是字符串,另一个是对象,等等。Nashorn 也支持类型化数组,但是它们的工作方式与 Java 数组完全不同。我将在本章的类型化数组部分讨论类型化数组。

创建数组

在 Nashorn 中创建数组有两种方法:

  • 使用数组文本
  • 使用Array对象

使用数组文本

数组文字是表示一个Array对象的表达式。数组文字也称为数组初始值设定项。它是用括号括起来的逗号分隔的表达式列表;列表中的每个表达式代表数组中的一个元素。以下是使用数组文字的示例:

// An array with no elements, also called an empty array

var emptyArray = [];

// An array with two elements

var names = ["Ken", "Li"];

// An array with four element. Elements are of mixed types.

var misc = [1001, "Ken", 1003, new Object()];

// Print the array length and its elements

print("Array's length: " + emptyArray.length + ", elements: " + emptyArray);

print("Array's length: " + names.length + ", elements: " + names);

print("Array's length: " + misc.length + ", elements: " + misc);

Array's length: 0, elements:

Array's length: 2, elements: Ken,Li

Array's length: 4, elements: 1001,Ken,1003,[object Object]

每个Array对象都包含一个toString()方法,该方法以字符串形式返回逗号分隔的数组元素列表。在添加到列表之前,每个元素都被转换为一个字符串。示例中调用了所有数组的toString()方法来打印它们的内容。

忽略数组文本中的尾随逗号。以下两个数组被认为是相同的。两者都有三个要素:

var empIds1 = [10, 20, 30];  // Without a trailing comma. empIds1.length is 3

var empIds2 = [10, 20, 30,]; // Same as [10, 20, 30]. empIds2.length is 3

数组文本中的元素是数组对象的索引属性。对于密集数组,第一个元素的索引为 0,第二个元素的索引为 1,第三个元素的索引为 3,依此类推。我将很快讨论稀疏数组的索引方案。考虑以下具有三个元素的密集阵列。图 7-1 显示了数组中元素的值及其索引。

var names = ["Fu", "Li", "Ho"]

A978-1-4842-0713-0_7_Fig1_HTML.jpg

图 7-1。

Array elements and their indexes in a three-element dense array

可以像访问对象属性一样访问数组元素。唯一的区别是元素的属性名是一个整数。比如在names数组中,names[0]是指第一个元素"Fu"names[1]是指第二个元素"Li"names[2]是指第三个元素"Ho"。下面的代码创建一个密集数组,并使用一个for循环来访问和打印数组的所有元素:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Print all array elements

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

names[0] = Fu

names[1] = Li

names[2] = Ho

向数组中添加元素等同于在不存在的索引处赋值。下面的代码创建一个包含三个元素的数组,并添加第四个和第五个元素。最后,代码打印数组中的所有元素:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Add fourth element

names[3] = "Su"; // Adds an element at index 3

// Add fifth element

names[4] = "Bo"; // Adds an element at index 4

// Print all array elements

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

names[4] = Bo

回想一下,数组是一个对象,因此您可以像向任何其他对象添加属性一样向数组添加属性。如果属性名不是索引,那么该属性将只是一个属性,而不是一个元素。非元素属性对数组的length没有贡献。下面的代码创建一个数组,添加一个元素和一个非元素属性,并打印详细信息:

// Create an array with three elements

var names = ["Fu", "Li", "Ho"]

// Add fourth element

names[3] = "Su"; // Adds an element at index 3

// Add a non-element property to the array. The property name is

// "nationality" that is not an index, so it does not define an element.

// Rather, it is a simply property.

names["nationality"] = "Chinese";

print("names.length = " + names.length);

// Print all array elements using a for loop

print("Using a for loop:");

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "] = " + names[i]);

}

// Print all properties of the array using a for..in loop

print("Using a for..in loop:");

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 4

Using a for loop:

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

Using a for..in loop:

names[0] = Fu

names[1] = Li

names[2] = Ho

names[3] = Su

names[nationality] = Chinese

关于这个例子,有几点需要注意:

  • 它创建了一个包含三个元素的数组,索引分别为 0、1 和 2。此时,数组的length为 3。
  • 它在索引 3 处添加了值为“Su”的元素。通过添加这个元素,数组的length自动增加到 4。
  • 它添加了一个名为“国籍”的属性这个属性只是一个属性,而不是一个元素,因为它的名称是一个不能转换为索引的字符串。添加该属性不会影响数组的length。也就是说,length停留在 4。
  • 当它使用 for 循环打印数组时,名为"nationality"的属性不会被打印,因为代码循环遍历索引,而不是所有属性。
  • 当它使用for..in循环时,所有元素和“nationality”属性都被打印出来,因为for..in循环遍历对象的所有属性。这证明了一个数组的所有元素都是属性,但所有属性都不是元素。

请注意,如果属性名可以转换为 0 到 2 32 -2(含)之间的整数,则该属性名被视为索引。检查属性名是否是索引的真正测试是应用以下条件。假设属性名为prop,是一个字符串。如果下面的表达式返回true,则属性名是一个索引;否则,它只是一个属性名:

ToString(ToUint32(prop)) = prop

这里,假设ToUint32()是将属性名转换成无符号 32 位整数的函数,而ToString()是将整数转换成字符串的函数。换句话说,如果将字符串属性名转换为无符号 32 位整数,然后再转换回字符串,从而得到原始的属性名,那么这样的属性名就是索引。如果属性名只是有效范围内的一个数字,如果不包含小数部分,则它是一个索引。下面的代码演示了这条规则:

// Create an array with two elements

var names = ["Fu", "Li"]

// Adds an element at the index 2

names[2.0] = "Su";

// Adds a property named "2.0", not an element at index 2

names["2.0"] = "Bo";

// Adds an element at index 3

names["3"] = "Do";

print("names.length = " + names.length);

// Print all properties of the array using a for..in loop

print("Using a for..in loop:");

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 4

Using a for..in loop:

names[0] = Fu

names[1] = Li

names[2] = Su

names[3] = Do

names[2.0] = Bo

可以将负数属性添加到数组中。注意,作为属性名的负数不符合索引的条件,所以它只是添加一个属性,而不是元素:

// Create an array with two elements

var names = ["Fu", "Li"]

// Adds property with the name "-1", not an element.

names[-1] = "Do"; // names.length is still 2

print("names.length = " + names.length);

names.length = 2

还可以使用数组文本创建稀疏数组。使用逗号而不指定元素列表中的元素会创建一个稀疏数组。请注意,在稀疏数组中,元素的索引是不连续的。下面的代码创建一个稀疏数组:

var names = ["Fu",,"Lo"];

names数组包含两个元素。它们位于索引 0 和 2 处。索引 1 处的元素丢失,这由两个连续的逗号表示。names阵的length是什么?是 3,不是 2。回想一下,数组的length总是大于所有元素的最大索引。数组中的最大索引是 2,所以length是 3。当你尝试阅读names[1]这个不存在的元素时会发生什么?读取names[1]被简单地视为从names对象中读取名为“1”的属性,而名为“1”的属性并不存在。回想一下Chapter 4,读取一个不存在的对象属性会返回undefined。因此,names[1]将简单地返回undefined,而不会导致任何错误。如果您给names[1]赋值,那么您将在索引 1 处创建一个新元素,并且该数组将不再是一个稀疏数组。以下代码显示了这条规则:

// Create a sparse array with 2 existing and 1 missing elements

var names = ["Fu",,"Lo"]; // names.length is 3

print("names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

// Add an element at index 1.

names[1] = "Do";  // names.length is still 3

print("names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

names.length = 3

names[0] = Fu

names[2] = Lo

names.length = 3

names[0] = Fu

names[1] = Do

names[2] = Lo

以下是稀疏数组的更多示例。注释解释了数组:

var names = [,];   // A sparse array. length = 1 and no elements

names = [,,];      // A sparse array. length = 2 and no elements

names = [,,,];     // A sparse array. length = 3 and no elements

names = [,,,7,,2]; // A sparse array. length = 6 and 2 elements

你能说出下面两个数组的区别吗?

var names1 = [,,];

var names2 = [undefined,undefined];

两个数组都有length 2。名为names1的数组是一个稀疏数组。names1中索引 0 和 1 处的元素不存在。读数names1[0]names1[1]将返回undefined。名为names2的数组是一个密集数组。names2中索引 0 和 1 处的元素存在,并且都被设置为undefined。读数names2[0]names2[1]将返回undefined

如何知道一个数组是否稀疏?在Array对象中没有检查稀疏数组的内置方法。你需要自己检查它,记住如果一个属性名是一个索引(从 0 到 ??)在数组中不存在,那么它就是一个稀疏数组。在遍历数组元素一节中,我将讨论几种检查稀疏数组的方法。

使用数组对象

Nashorn 包含一个名为Array的内置函数对象。它用于创建和初始化数组。它可以作为函数或构造函数调用。它作为函数或构造函数使用的方式是一样的。它的签名是:

Array(arg1, arg2, arg3,...)

Array对象可以接受零个或多个参数。它的初始化行为取决于传递的参数的数量和类型,这些参数可以分为三类:

  • 不传递任何参数
  • 一个参数被传递
  • 传递了两个或多个参数

不传递任何参数

当没有参数传递给Array构造函数时,它创建一个空数组,将数组的length设置为零:

var names1 = new Array(); // Same as: var names = [];

传递一个参数

当一个参数传递给Array构造函数时,参数的类型决定了新数组的创建方式:

  • 如果参数是一个数字,并且是 0 到 2 32 -1(含)范围内的整数,则该参数被视为数组的length。否则,抛出一个RangeError异常。
  • 如果参数不是数字,则创建一个数组,将传递的参数作为该数组的唯一元素。数组的length被设置为 1。

以下代码在 Nashorn 中创建了最大可能的数组:

var names = new Array(Math.pow(2, 32) -1); // The biggest possible array

print("names.length = " + names.length);

names.length = 4294967295

以下语句创建一个数组,其中length为 10。数组中还不存在任何元素:

var names = new Array(10);

以下数组创建表达式抛出了一个RangeError异常,因为参数是一个数字,并且它不是有效范围内的整数或者超出了范围:

var names1 = new Array(34.89);           // Not an integer

var names1 = new Array(Math.pow(2, 32)); // Out of range

var namess = new Array(-10);             // Out of range

以下代码将一个非数字参数传递给Array构造函数,该构造函数使用传递的参数作为唯一元素创建一个数组,并将数组的length设置为 1:

var names1 = new Array("Fu"); // Creates an array with one element "Fu"

var names2 = new Array(true); // Creates an array with one element true

传递两个或多个参数

当两个或更多参数被传递给Array构造函数时,它用指定的参数创建一个密集数组。length被设置为传递的参数数量。以下语句创建一个带有三个传递参数的数组,并将length设置为 3:

var names = new Array("Fu", "Li". "Do");

不能使用Array构造函数创建稀疏数组。在Array构造函数中使用连续逗号或尾随逗号会抛出一个SyntaxError异常:

var names1 = new Array("Fu", "Li",, "Do");  // A SyntaxError

var names2 = new Array("Fu", "Li", "Do", ); // A SyntaxError

您可以通过在不连续的索引处添加元素或删除现有元素来创建稀疏数组,从而使索引变得不连续。我将在下一节讨论删除数组的元素。下面的代码创建一个密集数组,并添加一个不连续的元素使其成为一个稀疏数组:

// Creates a dense array with elements at indexes 0 and 1.

var names = new Array("Fu", "Li");  // names.length is set to 2

print("After creating the array: names.length = " + names.length);

// Add an element at index 4, skipping index 2 and 3.

names[4] = "Do"; // names.length is set to 5, making names a sparse array

print("After adding an element at index 4: names.length = " + names.length);

for(var prop in names) {

print("names[" + prop + "] = " + names[prop]);

}

After creating the array: names.length = 2

After adding an element at index 4: names.length = 5

names[0] = Fu

names[1] = Li

names[4] = Do

删除数组元素

删除数组元素或数组的非元素属性与删除对象的属性相同。使用delete操作符删除一个数组元素。如果从密集数组的中间或开头删除一个元素,数组将变得稀疏。下面的代码显示了如何从数组中删除元素:

// Creates a dense array with elements at indexes 0, 1, and 2.

var names = new Array("Fu", "Li", "Do");

print("Before deleting:");

print("names.length = " + names.length + ", Elements = " + names);

// Delete the element at index 1

delete names[1]; // names.length remains 3

print("AFter deleting:");

print("names.length = " + names.length + ", Elements = " + names);

Before deleting:

names.length = 3, Elements = Fu,Li,Do

AFter deleting:

names.length = 3, Elements = Fu,,Do

您可以将数组的元素设置为不可配置和不可写,这样它们就不能被删除和修改。删除不可配置的元素没有任何效果。在严格模式下,删除不可配置的元素会产生错误。下面的代码演示了这一点:

var names = new Array("Fu", "Li", "Do");

// Make the element at index 1 non-configurable

Object.defineProperty(names, "1", {configurable: false});

print("Before deleting:");

print("names.length = " + names.length + ", Elements = " + names);

delete names[1]; // Will not delete "Li" as it is non-configurable.

print("AFter deleting:");

print("names.length = " + names.length + ", Elements = " + names);

Before deleting:

names.length = 3, Elements = Fu,Li,Do

AFter deleting:

names.length = 3, Elements = Fu,Li,Do

数组长度

每个Array对象都有一个名为length的属性,当在数组中添加和删除元素时会自动维护该属性。length属性使数组不同于其他类型的对象。对于密集数组,length比数组中最大的索引大 1。对于稀疏数组,它保证大于所有元素(现有的和缺失的)的最大索引。

数组的length属性是可写的。也就是说,您也可以在代码中更改它。如果您将length设置为大于当前值的值,length将被更改为新值,从而在末尾创建一个稀疏数组。如果将length设置为一个小于其当前值的值,从末尾开始的所有元素都将被删除,直到找到一个大于或等于新的length值的不可删除元素。也就是说,将length设置为较小的值会使数组收缩为不可删除的元素。下面的例子将阐明这一规则:

var names = new Array("Fu", "Li", "Do", "Ho");

print("names.length = " + names.length + ", Elements = " + names);

print("Setting length to 10...");

names.length = 10;

print("names.length = " + names.length + ", Elements = " + names);

print("Setting length to 0...");

names.length = 0;

print("names.length = " + names.length + ", Elements = " + names);

print("Recreating the array...");

names = new Array("Fu", "Li", "Do", "Ho");

print("names.length = " + names.length + ", Elements = " + names);

print('Making "Do" non-configurable...');

// Makes "Do" non-configurable (non-deletable)

Object.defineProperty(names, "2", {configurable:false});

print("Setting length to 0...");

names.length = 0; // Will delete only "Ho" as "Do" is non-deletable

print("names.length = " + names.length + ", Elements = " + names);

names.length = 4, Elements = Fu,Li,Do,Ho

Setting length to 10...

names.length = 10, Elements = Fu,Li,Do,Ho,,,,,,

Setting length to 0...

names.length = 0, Elements =

Recreating the array...

names.length = 4, Elements = Fu,Li,Do,Ho

Making "Do" non-configurable...

Setting length to 0...

names.length = 3, Elements = Fu,Li,Do

Array对象的length属性是可写的、不可数的和不可配置的。如果不希望有人在代码中更改它,可以将其设置为不可写。在数组中添加和删除元素时,不可写的length属性仍然会自动改变。以下代码显示了这条规则:

var names = new Array("Fu", "Li", "Do", "Ho");

// Make the length property non-writable.

Object.defineProperty(names, "length", {writable:false});

// The length property cannot be changed directly anymore

names.length = 0; // No effects

// Add a new element

names[4] = "Nu"; // names.length changes from 4 to 5

迭代数组元素

如果您对迭代数组的所有属性(包括元素)感兴趣,可以简单地使用for..infor..each..in语句。这些语句不应该以任何特定的顺序迭代数组。正如这一节的标题所暗示的,我将讨论如何只迭代数组的元素,尤其是当数组是稀疏的时候。如果您只向数组中添加元素(而不是任何非元素属性),这是您在大多数情况下都会做的,那么使用for..infor..each..in语句对于密集和稀疏都很好。

使用 for 循环

如果您知道数组是密集的,您可以使用简单的for循环来迭代数组,如下所示:

// Create a dense array

var names = new Array("Fu", "Li", "Do");

// Use a for loop to iterate all elements of an array

for(var i = 0, len = names.length; i < len; i++) {

print("names[" + i + "]=" + names[i]);

}

names[0]=Fu

names[1]=Li

names[2]=Do

如果你有一个稀疏的数组,for循环不起作用。如果您尝试访问稀疏数组中缺少的元素,它将返回undefined,但不会告诉您是缺少元素还是现有元素的值是undefined。您可以通过使用in操作符来检查被迭代的索引是否存在,从而摆脱这个限制。如果索引存在,则元素存在;如果索引不存在,则它是稀疏数组中缺少的元素。下面的代码演示了这种方法:

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Use a for lop to iterate all elements of an array

for (var i = 0, len = names.length; i < len; i++) {

// Check if the index being visited exists in the array

if (i in names) {

print("names[" + i + "]=" + names[i]);

}

}

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

考虑下面的代码。它创建一个只有一个元素的稀疏数组。该数组是 Nashorn 中可能的最大数组,元素被添加到数组的最后一个索引处。实际上,你永远也不会有这个大数组。我使用它只是为了证明使用for循环并不是访问稀疏数组中所有元素的最有效方式:

// Create an empty array

var names = new Array();

// Add one element to the end of the biggest possible  array

names[4294967294] = "almost lost";

// Use a for loop to iterate all elements of an array

for (var i = 0, len = names.length; i < len; i++) {

// Check if the index being visited exists in the array

if (i in names) {

print("names[" + i + "]=" + names[i]);

}

}

在这个例子中使用for循环是非常低效的,因为它必须访问 4294967293 个索引,才能到达最后一个有值的索引。只访问数组中的一个元素花费的时间太长。我将很快使用for..in循环解决这个运行缓慢的for循环问题。

使用 forEach()方法

Array.prototype对象包含一个名为forEach()的方法,该方法按照升序为数组中的每个元素调用回调函数。它只访问稀疏数组中的现有元素。它的签名是:

forEach (callbackFunc [, thisArg])

这里,callbackFunc是按升序对数组中的每个元素调用一次的函数。传递给它三个参数:元素的值、元素的索引和被迭代的对象。thisArg是可选的;如果指定了它,它将被用作每次调用callbackFuncthis值。

forEach()方法在开始执行之前设置它将访问的索引范围。如果在执行过程中添加了超出该范围的元素,则不会访问这些元素。如果初始设置范围内的元素被删除,这些元素也不会被访问:

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Define the callback function

var visitor = function (value, index, array) {

print("names[" + index + "]=" + value);

};

// Print all elements

names.forEach(visitor);

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

如果您使用forEach()方法访问一个大的稀疏数组,您将会遇到与使用我在上一节中讨论的for循环相同的问题。

使用 for-in 循环

让我们使用for..in循环来解决访问长稀疏数组中元素的低效问题。使用for..in循环带来了另一个问题,它遍历数组的所有属性,而不仅仅是元素。但是,它不会迭代丢失的元素。您需要一种方法来区分简单属性和现有元素。只需使用数组索引的定义就可以做到这一点。数组索引是一个介于 0 和 2 32 -2 之间的整数。如果属性是数组索引,则它是一个元素;否则,它只是一个属性,而不是一个元素。清单 7-1 包含一个名为isValidArrayIndex()的函数的代码。该函数将属性名作为参数。如果属性名是有效的数组索引,则返回true;否则,它返回false

清单 7-1。isValidArrayIndex()函数的代码

// arrayutil.js

var UPPER_ARRAY_INDEX = Math.pow(2, 32) - 2;

Object.defineProperty(this, "UPPER_ARRAY_INDEX", {writable: false});

function isValidArrayIndex(prop) {

// Convert the prop to a Number

var numericProp = Number(prop);

// Check if prop is a number

if (String(numericProp) === prop) {

// Check if is an integer

if (Math.floor(numericProp) === numericProp) {

// Check if it is in the valid array index range

if (numericProp >= 0 &&                             numericProp <= UPPER_ARRAY_INDEX) {

return true;

}

}

}

return false;

}

下面的代码使用for..in循环只迭代稀疏数组的元素。它利用isValidArrayIndex()函数来确定一个属性是否是有效的数组索引。请注意,代码在数组中最大可能的索引处添加了一个元素。for..in循环非常快速地到达数组的所有元素。

// load the script that contains isValidArrayIndex() function

load("arrayutil.js");

// Create a sparse array with an element set to undefined

var names = ["Fu", "Li", , "Do", undefined, , "Lu"];

// Add some properties to the array

names["nationality"] = "Chinese";  // A property

names[4294967294] = "almost lost"; // An element at the largest

// possible array index

names[3.2] = "3.2";   // A property

names[7.00] = "7.00"; // An element

// Print all elements, ignoring the non-element properties

for(var prop in names) {

if (isValidArrayIndex(prop)) {

// It is an element

print("names[" + prop + "]=" + names[prop]);

}

}

names[0]=Fu

names[1]=Li

names[3]=Do

names[4]=undefined

names[6]=Lu

names[7]=7.00

names[4294967294]=almost lost

检查数组

Array对象包含一个isArray()静态方法,可以用来检查一个对象是否是一个数组。下面的代码演示了它的用法:

var obj1 = [10, 20];     // An array

var obj2 = {x:10, y:20}; // An object, but not an array

print("Array.isArray(obj1) =",  Array.isArray(obj1));

print("Array.isArray(obj2) =",  Array.isArray(obj2));

Array.isArray(obj1) = true

Array.isArray(obj2) = false

多维数组

Nashorn 有任何特殊的构造来支持多维数组。您需要使用数组的数组(曲折数组)来创建多维数组,其中数组的元素可以是数组。通常,您需要嵌套的for循环来填充和访问多维数组的元素。下面的代码演示了如何创建、填充和打印 3x3 矩阵的元素。代码使用Array对象的join()方法,使用制表符作为分隔符来连接数组的元素:

var ROWS = 3;

var COLS = 3;

// Create a 3x3 array, so you pre-allocate the memory

var matrix = new Array(ROWS);

for(var i = 0; i < ROWS; i++) {

matrix[i] = new Array(COLS);

}

// Populate the array

for(var i = 0; i < ROWS; i++) {

for(var j = 0; j < COLS; j++) {

matrix[i][j] = i + "" + j;

}

}

// Print the array elements

for(var i = 0; i < ROWS; i++) {

var rowData = matrix[i].join("\t");

print(rowData);

}

00        01        02

10        11        12

20        21        22

数组对象的方法

Array.prototype对象定义了几个方法,作为所有Array对象的实例方法。在这一节中,我将讨论这些方法。所有数组方法都是有意泛型的,因此它们不仅可以应用于数组,还可以应用于任何类似数组的对象。

串联元素

concat(arg1, arg2,...)方法创建并返回一个新数组,该数组包含调用该方法的数组对象的元素,后跟指定的参数。如果一个参数是一个数组,数组的元素被连接起来。如果参数是嵌套数组,它将被展平一级。此方法不会修改原始数组和作为参数传递的数组。如果原始数组或参数包含对象,则创建对象的浅表副本。使用concat()方法的例子如下:

var names = ["Fu", "Li"];

// Assigns ["Fu", "Li", "Do", "Su"] to names2

var names2 = names.concat("Do", "Su");

// Assigns ["Fu", "Li", "Do", "Su", ["Lu", "Zu"], "Yu"] to names3

var names3 = names.concat("Do", ["Su", ["Lu","Zu"]], "Yu");

连接数组元素

join(separator)方法将数组的所有元素转换成字符串,使用separator将它们连接起来,并返回结果字符串。如果未指定separator,则使用逗号作为分隔符。该方法作用于数组的所有元素,从索引 0 到等于length - 1的索引。如果元素是undefinednull,则使用空字符串。以下是使用join()方法的一些例子:

var names = ["Fu", "Li", "Su"];

var namesList1 = names.join();    // Assigns "Fu,Li,Su" to namesList1

var namesList2 = names.join("-"); // Assigns "Fu-Li-Su" to namesList2

var ids = [10, 20, , 30, undefined, 40, null]; // A sparse array

var idsList1 = ids.join();    // Assigns "10,20,,30,,40," to idsList1

var idsList2 = ids.join("-"); // Assigns "10-20--30--40-" to idsList2

反转数组元素

方法以相反的顺序重新排列数组的元素并返回数组。使用这种方法的例子如下:

var names = ["Fu", "Li", "Su"];

names.reverse(); // Now, the names array contains ["Su","Li","Fu"]

// Assigns "Fu,Li,Su" to reversedList

var reversedList = names.reverse().join();

分割数组

slice(start, end)方法从startend之间的索引返回一个数组,该数组包含原始数组的子数组。如果startend为负,则分别作为start + lengthend + length处理,其中length是数组的长度。startend都被限制在 0 和length之间(包括 0 和 ??)。如果未指定end,则假定为length。如果startend(互斥)包含一个稀疏范围,那么得到的子数组将是稀疏的。以下是使用slice()方法的例子:

var names = ["Fu", "Li", "Su"];

// Assigns ["Li","Su"] to subNames1\. end = 5 will be replaced with end = 3.

var subNames1 = names.slice(1, 5);

// Assigns ["Li","Su"] to subNames2\. start = -1 is used as start = 1 (-2 + 3).

var subNames12 = names.slice(-2, 3);

var ids = [10, 20,,,30, 40, 40]; // A sparse array

// Assigns [20,,,30,40] to idsSubList whose length = 5

var idsSubList = ids.slice(1, 6);

拼接数组

根据传递的参数,splice()方法可以执行插入和/或删除。它的签名是:

splice (start, deleteCount, value1, value2,...)

该方法删除从索引start开始的deleteCount元素,并插入指定的参数(value1value2等)。)在索引start。它返回一个数组,其中包含从原始数组中删除的元素。在删除和/或插入之后,现有元素的索引被调整,因此它们是连续的。如果deleteCount为 0,则插入指定值而不删除任何元素。以下是使用该方法的示例:

var ids = [10, 20, 30, 40, 50];

// Replace 10 and 20 in the array with 100 and 200

var deletedIds  = ids.splice(1, 2, 100, 200);

print("ids = " + ids);

print("deletedIds = " + deletedIds);

// Keep the first 3 elements and delete the rest

var deletedIds2  = ids.splice(3, 2);

print("ids = " + ids);

print("deletedIds2 = " + deletedIds2);

ids = 10,100,200,40,50

deletedIds = 20,30

ids = 10,100,200

deletedIds2 = 40,50

对数组排序

sort(compareFunc)方法就地对数组进行排序,并返回排序后的数组。compareFunc参数是一个有两个参数的函数,是可选的。如果未指定,则数组按字母顺序排序,如果需要,在比较期间将元素转换为字符串。未定义的元素被排序到末尾。

如果指定了compareFunc,它将被传递两个元素,它的返回值将决定这两个元素的顺序。假设它被传递了 x 和 y,如果 x < y 返回负值,如果x = y返回零,如果x > y返回正值。下面的代码通过不指定compareFunc对一个整数数组进行升序排序。随后,它通过指定一个compareFunc对同一个数组进行降序排序:

var ids = [30, 10, 40, 20];

print("ids = " + ids);

// Sort the array

ids.sort();

print("Sorted ids = " + ids);

// A comparison function to sort ids in descending order

var compareDescending = function (x, y) {

if (x > y) {

return -1;

}

else if (x < y) {

return 1;

}

else {

return 0;

}

};

ids.sort(compareDescending);

print("Sorted in descending order, ids = " + ids);

ids = 30,10,40,20

Sorted ids = 10,20,30,40

Sorted in descending order, ids = 40,30,20,10

在端点添加和移除元素

以下四种方法允许您在数组的开头和结尾添加和移除元素:

  • unshift(value1, value2,...):unshift()方法将参数添加到数组的前面,因此它们出现在数组开头的顺序与它们作为参数出现的顺序相同。它返回数组的新的length。调整现有元素的索引以为新元素腾出空间。
  • shift():shift()方法移除并返回数组的第一个元素,将所有其他元素左移一个位置。
  • push(value1, value2,...):push()方法与unshift()方法的工作原理相同,除了它在数组的末尾添加元素。
  • pop():pop()方法与shift()方法工作方式相同,除了它移除并返回数组的最后一个元素

以下是使用这些方法的示例:

var ids = [10, 20, 30];

print("ids: " + ids);

ids.unshift(100, 200, 300);

print("After ids.unshift(100, 200, 300): " + ids);

ids.shift();

print("After ids.shift(): " + ids);

ids.push(1, 2, 3);

print("After ids.push(1, 2, 3): " + ids);

ids.pop();

print("After ids.pop(): " + ids);

ids: 10,20,30

After ids.unshift(100, 200, 300): 100,200,300,10,20,30

After ids.shift(): 200,300,10,20,30

After ids.push(1, 2, 3): 200,300,10,20,30,1,2,3

After ids.pop(): 200,300,10,20,30,1,2

使用这些方法,您可以实现堆栈、队列和双端队列。让我们使用push()pop()方法实现一个堆栈。清单 7-2 包含了Stack对象的代码。它将堆栈的数据保存在私有的Array对象中。它提供了isEmpty()push()pop()peek()方法来执行堆栈操作。它覆盖了Object.prototypetoString()方法,以字符串形式返回堆栈中的当前元素。清单 7-3 包含了测试Stack对象的代码。

清单 7-2。堆栈对象声明

// stack.js

// Define the constructor for the Stack object

function Stack(/*varargs*/) {

// Define a private array to keep the stack elements

var data = new Array();

// If any arguments were passed to the constructor, add them to the stack

for (var i in arguments) {

data.push(arguments[i]);

}

// Define methods

this.isEmpty = function () {

return (data.length === 0);

};

this.pop = function () {

if (this.isEmpty()) {

throw new Error("Stack is empty.");

}

return data.pop();

};

this.push = function (arg) {

data.push(arg);

return arg;

};

this.peek = function () {

if (this.isEmpty()) {

throw new Error("Stack is empty.");

}

else {

return data[data.length - 1];

}

};

this.toString = function () {

return data.toString();

};

}

清单 7-3。测试堆栈对象

// stacktest.js

load("stack.js");

// Create a Stack with initial 2 elements

var stack = new Stack(10, 20);

print("Stack = " + stack);

// Push an element

stack.push(40);

print("After push(40), Stack = " + stack);

// Pop two elements

stack.pop();

stack.pop();

print("After 2 pops, Stack = " + stack);

print("stack.peek() = " + stack.peek());

print("stack.isEmpty() = " + stack.isEmpty());

// Pop the last element

stack.pop();

print("After another pop(), stack.isEmpty() = " + stack.isEmpty());

Stack = 10,20

After push(40), Stack = 10,20,40

After 2 pops, Stack = 10

stack.peek() = 10

stack.isEmpty() = false

After another pop(), stack.isEmpty() = true

搜索数组

有两种方法,indexOf()lastIndexOf(),可以让您在数组中搜索指定的元素。他们的签名是:

  • indexOf(searchElement, fromIndex)
  • lastIndexOf(searchElement, fromIndex)

indexOf()方法从fromIndex开始在数组中搜索指定的searchElement。如果没有指定fromIndex,则假定为 0,这意味着搜索整个数组。该方法使用===运算符返回数组中等于searchElement的第一个元素的索引。如果没有找到searchElement,则返回-1。

lastIndexOf()方法的工作方式与indexOf()方法相同,除了它从末尾到开头执行搜索。也就是说,它返回数组中最后找到的元素的索引,该索引等于searchElement。下面的代码显示了如何使用这些方法:

var ids = [10, 20, 30, 20];

print("ids = " + ids);

print("ids.indexOf(20) = " + ids.indexOf(20));

print("ids.indexOf(20, 2) = " + ids.indexOf(20, 2));

print("ids.lastIndexOf(20) = " + ids.lastIndexOf(20));

print("ids.indexOf(25) = " + ids.lastIndexOf(25));

ids = 10,20,30,20

ids.indexOf(20) = 1

ids.indexOf(20, 2) = 3

ids.lastIndexOf(20) = 3

ids.indexOf(25) = -1

评估谓词

您可以检查一个数组中的所有或部分元素的谓词是否计算为true。以下两种方法允许您执行此操作:

  • every(predicate, thisArg)
  • some(predicate, thisArg)

第一个参数predicate是一个函数,对数组中的每个现有元素调用一次。向该函数传递三个参数:元素的值、元素的索引和数组对象。如果指定了第二个参数thisArg,它将被用作由predicate指定的函数调用的this值。

如果指定的predicate函数为数组中的每个现有元素返回真值,则every()方法返回true。否则返回false。一旦predicate返回一个元素的 falsy 值,该方法就返回false

如果predicate返回至少一个元素的真值,则some()方法返回true。否则返回false。一旦predicate返回一个元素的真值,该方法就返回true。以下示例显示如何检查数字数组是否包含任何/所有偶数/奇数:

var ids = [10, 20, 30, 20];

print("ids = " + ids);

var hasAnyEven = ids.some(function (value, index, array) {

return value %2 === 0;

});

var hasAllEven = ids.every(function (value, index, array) {

return value %2 === 0;

});

var hasAnyOdd = ids.some(function (value, index, array) {

return value %2 === 1;

});

var hasAllOdd = ids.every(function (value, index, array) {

return value %2 === 1;

});

print("ids has any even numbers: " + hasAnyEven);

print("ids has all even numbers: " + hasAllEven);

print("ids has any odd numbers: " + hasAnyOdd);

print("ids has all odd numbers: " + hasAllOdd);

ids = 10,20,30,20

ids has any even numbers: true

ids has all even numbers: true

ids has any odd  numbers: false

ids has all odd  numbers: false

将数组转换为字符串

你可以用任何方式将数组转换成字符串。然而,toString()toLocaleString()方法提供了两个内置的实现来将数组元素转换成字符串。toString()方法返回一个字符串,该字符串是在没有指定任何分隔符的情况下通过调用数组上的join()方法返回的。也就是说,它以字符串形式返回元素列表,其中元素用逗号分隔。toLocaleString()方法调用所有数组元素的toLocaleString()方法,使用特定于本地的分隔符连接返回的字符串,并返回最终的字符串。每当您将数组对象用作print()方法的参数或将其与字符串连接操作符一起使用时,您就一直在使用Array对象的toString()方法。

数组的流状处理

Java 8 引入了 Streams API,允许您使用 map-filter-reduce 等几种模式将 Java 集合作为流进行处理。Nashorn 中的Array对象提供了将类似流的处理应用于数组的方法。这些方法是:

  • map()
  • filter()
  • reduce()
  • reduceRight()
  • forEach()
  • some()
  • every()

我已经在前一节详细讨论了最后三种方法。我将在本节讨论其他方法。

方法将一个数组的元素映射到另一个值,并返回一个包含映射值的新数组。原始数组不会被修改。它的签名是:

map(callback, thisArg)

第一个参数callback是为数组中的每个元素调用的函数;向该函数传递三个参数:元素的值、元素的索引和数组本身。函数的返回值是新的(映射的)数组的元素。第二个参数用作函数调用中的this值。下面是一个使用map()方法的例子。它将数字数组的每个元素映射到它们的正方形:

var nums = [1, 2, 3, 4, 5];

// Map each element in the nums array to their squares

var squaredNums = nums.map(function (value, index, data) value * value);

print(nums);

print(squaredNums);

1,2,3,4,5

1,4,9,16,25

filter()方法返回一个新数组,其中包含传递谓词的原始数组的元素。它的签名是:

filter(callback, thisArg)

第一个参数callback是为数组中的每个元素调用的函数;向该函数传递三个参数:元素的值、元素的索引和数组本身。如果函数返回一个真值,则该元素包含在返回的数组中;否则,该元素将被排除。第二个参数用作函数调用中的this值。下面的代码展示了如何使用filter()方法从数组中过滤出偶数:

var nums = [1, 2, 3, 4, 5];

// Filter out even numbers, keep odd numbers only

var oddNums = nums.filter(function (value, index, data) (value % 2 !== 0));

print(nums);

print(oddNums);

1,2,3,4,5

1,3,5

reduce()方法对数组应用归约操作,将其归约为单个值,比如计算一个数字数组中所有元素的总和。它返回一个计算值。它的签名是:

reduce(callback, initialValue)

第一个参数callback是用四个参数调用的函数:前一个值、当前值、当前索引和数组本身。

如果未指定initialValue:

  • 对于第一次调用callback,数组的第一个元素作为前一个值传递,第二个元素作为当前值传递;第二个元素的索引作为索引传递
  • 对于对callback的后续调用,从先前调用返回的值作为先前的值传递。当前值和索引是当前元素的值和索引

如果指定了initialValue:

  • 对于第一次调用callbackinitialValue作为前一个值传递,第一个元素作为当前值传递;第一个元素的索引作为索引传递
  • 对于对callback的后续调用,前一次调用返回的值作为前一次值传递;当前值和索引是当前元素的值和索引

除了从头到尾处理数组元素之外,reduceRight()方法的工作方式与reduce()方法相同。下面的代码展示了如何使用reduce()reduceRight()方法来计算数组中所有数字的总和,以及连接字符串数组的元素:

var nums = [1, 2, 3, 4, 5];

// Defines a reducer function to compute sum of elements of an array

var sumReducer = function(previous, current, index, data) {

return previous + current;

};

var sum = nums.reduce(sumReducer);

print("Numbers :" + nums);

print("Sum: " + sum);

// Defines a reducer function to concatenate elements of an array

var concatReducer = function(previous, current, index, data) {

return previous + "-" + current;

};

var names = ["Fu", "Li", "Su"];

var namesList = names.reduce(concatReducer);

var namesListRight = names.reduceRight(concatReducer);

print("Names: " + names);

print("Names Reduced List: " + namesList);

print("Names Reduced Right List: " + namesListRight);

Numbers :1,2,3,4,5

Sum: 15

Names: Fu,Li,Su

Names Reduced List: Fu-Li-Su

Names Reduced Right List: Su-Li-Fu

您还可以将这些方法链接起来,对数组执行复杂的处理。以下代码显示了如何在一条语句中计算数组中所有正奇数的平方和:

var nums = [-2, 1, 2, 3, 4, 5, -11];

// Compute the sum of squares of all positive odd numbers

var sum = nums.filter(function (value, index, data) value > 0 && value % 2 !== 0)

.map(function (value, index, data) value * value)

.reduce(function (prev, curr, index, data) prev + curr, 0);

print("Numbers: " + nums);

print("Sum of squares of positive odd elements: " + sum);

Numbers: -2,1,2,3,4,5,-11

Sum of squares of positive odd elements: 35

类似数组的对象

数组有两个不同于常规对象的特征:

  • 它有一个length属性
  • 它的元素有特殊的整数属性名

您可以定义任何具有这两个特征的对象,并将它们称为类数组对象。下面的代码定义了一个名为list的对象,它是一个类似数组的对象:

// Creates an array-like object

var list = {"0":"Fu", "1":"Su", "2":"Li", length:3};

Nashorn 包含一些类似数组的对象,比如 String 和arguments对象。Array.prototype对象中的大多数方法都是通用的,这意味着它们可以在任何类似数组的对象上调用,而不一定只在数组上调用。下面的代码显示了如何在类似数组的对象上调用Array.prototype对象的join()方法:

// An array-like object

var list = {"0":"Fu", "1":"Su", "2":"Li", length:3};

var joinedList = Array.prototype.join.call(list, "-");

print(joinedList);

Fu-Su-Li

字符串对象也是一个类似数组的对象。它维护一个只读的length属性,每个字符都有一个索引作为它的属性名。您可以对字符串执行以下类似数组的操作:

// A String obejct

var str = new String("ZIP");

print("str[0] = " + str[0]);

print("str[1] = " + str[1]);

print("str[2] = " + str[2]);

print('str["length"] = ' + str["length"]);

使用 String 对象的charAt()length()方法可以获得相同的结果。下面的代码将字符串转换为大写,删除英文字母的元音字母,并使用连字符作为分隔符连接所有字符。所有这些都是使用Array.prototype对象上的方法完成的,就好像字符串是一个字符数组一样:

// A String object

var str = new String("Nashorn");

// Use the map-filter-reduce patern on the string

var newStr = Array.prototype.map.call(str, (function (v, i, d) v.toUpperCase()))

.filter(function (v, i, d) (v !== "A" && v !== 'E' && v !== 'I' && v !== 'O' && v !== 'U'))

.reduce(function(prev, cur, i, data) prev + "-" + cur);

print("Original string: " + str);

print("New string: " + newStr);

Original string: Nashorn

New string: N-S-H-R-N

类型化数组

Nashorn 中的类型化数组是类似数组的对象。它们提供了被称为缓冲区的原始二进制数据的视图。同一个缓冲区可以有多个类型化视图。假设你有一个 4 字节的缓冲区。您可以拥有一个 8 位有符号整数类型的缓冲区数组视图,它将表示四个 8 位有符号整数。同时,您可以拥有同一缓冲区的 32 位无符号整数类型数组视图,该视图可以表示一个 32 位无符号整数。

Tip

类型化数组是一个类似数组的对象,提供内存中原始二进制数据的类型化视图。Nashorn 中类型化数组实现的规范可以在 https://www.khronos.org/registry/typedarray/specs/latest/ 找到。

类型化数组中的缓冲区由一个ArrayBuffer对象表示。直接或间接地使用一个ArrayBuffer对象,您可以创建一个类型化的数组视图。一旦有了类型化数组视图,就可以使用类似数组的语法写入或读取类型化数组视图支持的特定类型的数据。在下一节中,我将讨论如何使用ArrayBuffer对象。在随后的章节中,我将讨论不同类型的类型化数组视图(或者简称为类型化数组)。

ArrayBuffer 对象

一个ArrayBuffer对象表示原始二进制数据的固定长度缓冲区。ArrayBuffer的内容没有类型。您不能直接修改ArrayBuffer中的数据;为此,您必须在其上创建并使用一个类型化视图。它包含一些属性和方法,可以将其内容复制到另一个ArrayBuffer中,并查询其大小。

ArrayBuffer构造函数接受一个参数,即缓冲区的长度(以字节为单位)。创建后,ArrayBuffer对象的长度不可更改。该对象有一个名为byteLength的只读属性,表示缓冲区的长度。以下语句创建一个 32 字节的缓冲区,并打印其长度:

// Create a ArrayBuffer of 32 bytes

var buffer = new ArrayBuffer(32);

// Assigns 32 to len

var len = buffer.byteLength;

Tip

当您创建一个ArrayBuffer时,它的内容被初始化为零。在创建时,无法用非零值初始化其内容。ArrayBuffer中的每个字节使用一个从零开始的索引。第一个字节的索引为 0,第二个为 1,第三个为 2,依此类推。

Nashorn 实现的类型化数组规范在ArrayBuffer对象中包含一个isView(args)静态方法。不过在 Java 8u40 中似乎并没有被 Nashorn 实现。一个错误已经在bugs.openjdk.java.net/browse/JDK-8061959备案。如果指定的参数表示数组缓冲区视图,该方法返回true。例如,如果您将一个类似于Int8ArrayInt16ArrayDataView等的对象传递给这个方法,它将返回true,因为这些对象表示一个数组缓冲区视图。如果您向该方法传递一个简单的对象或原始值,它将返回false。下面的代码显示了如何使用此方法。注意,从 Java 8u40 开始,这个方法在 Nashorn 中不存在,代码将抛出一个异常:

// Creates an array buffer view of length 4

var int8View = new Int8Array(4);

// Assigns true to isView1

var isView1 = ArrayBuffer.isView(int8View);

// Assigns false to isView2

var isView2 = ArrayBuffer.isView({});

ArrayBuffer对象包含一个slice(start, end)方法,该方法创建并返回一个新的ArrayBuffer,它的内容是从索引startend的原始ArrayBuffer的副本,包含在内,不包含在内。如果startend为负,则表示从缓冲区末端开始的索引。如果未指定end,则默认为ArrayBufferbyteLength属性。startend都夹在 0 和byteLength—1之间。以下代码显示了如何使用slice()方法:

// Create an ArrayBuffer of 4 bytes. Bytes have indexes 0, 1, 2, and 3

var buffer = new ArrayBuffer(4);

// Manipulate buffer using one of the typed views here...

// Copy the last 2 bytes from buffer to buffer2

var buffer2 = buffer.slice(2, 4);

// Copy the bytes from buffer from index 1 to the end (last 3 bytes)

// to buffer3

var buffer3 = buffer.slice(1);

数组缓冲区的视图

一个ArrayBuffer有两种视图:

  • 类型化数组视图
  • 数据视图视图

类型化数组

类型化数组视图是类似数组的对象,处理特定类型的值,如 32 位有符号整数。它们提供了向ArrayBuffer读写特定类型数据的方法。在类型化数组视图中,所有数据值的大小都相同。表 7-1 包含类型化数组视图的列表、它们的大小和描述。表中的大小是类型化数组视图的一个元素的大小。例如,Int8Array包含长度为 1 个字节的元素。你可以把Int8Array想象成 Java 中的byte数组类型,把Int16Array想象成 Java 中的short数组类型,把Int32Array想象成 Java 中的int数组类型。

Tip

类型化数组是一个固定长度、类似密集数组的对象,而Array对象是一个可变长度的数组,可以是稀疏的也可以是密集的。

表 7-1。

The List of Typed Array Views, Their Sizes, and Descriptions

类型化数组视图 字节大小 描述
Int8Array one 8 位二进制补码有符号整数
Uint8Array one 8 位无符号整数
Uint8ClampedArray one 8 位无符号整数(箝位)
Int16Array Two 16 位二进制补码有符号整数
Uint16Array Two 16 位无符号整数
Int32Array four 32 位二进制补码有符号整数
Uint32Array four 32 位无符号整数
Float32Array four 32 位 IEEE 浮点
Float64Array eight 64 位 IEEE 浮点

让我们区分两种数组类型:Uint8ArrayUint8ClampedArray。两个数组中的元素都包含从 0 到 255 的 8 位无符号整数。Uint8Array使用模 256 来存储数组中的值,而Uint8ClampedArray将值固定在 0 到 255 之间(包括 0 和 255)。例如,如果您在一个UInt8array中存储 260,则存储 4,因为 260 对 256 取模是 4。如果你在一个Uint8ClampedArray中存储 260,存储 255 是因为 260 大于 255,上限被箝位在 255。类似地,存储-2 在 a Uint8Array中存储 254,在Uint8ClampedArray中存储 0。

每个类型化数组视图对象定义了以下两个属性:

  • BYTES_PER_ELEMENT
  • name

属性包含类型数组元素的大小,以字节为单位。它在视图类型中的值与表 0-1 中“以字节为单位的大小”栏中的值相同。name属性包含一个作为视图名称的字符串,它与表中的“类型化数组视图”列相同。例如,Int16Array.BYTES_PR_ELEMNET是 2,Int16Array.name"Int16Array"

所有类型化数组视图都提供了四个构造函数。下面是Int8Array的四个构造函数。您可以用其他类型化数组视图的名称替换名称Int8Array,以获得它们的构造函数:

  • Int8Array(arrayBuffer, byteOffset, elementCount)
  • Int8Array(length)
  • Int8Array(typedArray)
  • Int8Array(arrayObject)

第一个构造函数从一个ArrayBuffer创建一个类型化数组视图。您可以创建指定ArrayBuffer的完整或部分视图。如果byteOffsetelementCount未指定,它会创建一个完整的视图ArrayBuffer. byteOffset是缓冲区中从开始的字节偏移量,elementCount是将占用缓冲区的数组元素的数量。

第二个构造函数将类型化数组的length作为一个参数,创建一个适当大小的ArrayBuffer,并返回完整的ArrayBuffer的视图。

第三和第四个构造函数让你从另一个类型化数组和一个Array对象创建一个类型化数组;新类型化数组的内容从指定数组的内容初始化。创建一个适当大小的新ArrayBuffer来保存复制的内容。

以下代码显示了如何使用不同的构造函数创建Int8Array对象(字节数组):

// Create an ArrayBuffer of 8 bytes

var buffer = new ArrayBuffer(8);

// Create an Int8Array that is a full view of buffer

var fullView = new Int8Array(buffer);

// Create an Int8Array that is the first half view of buffer

var firstHalfView = new Int8Array(buffer, 0, 4);

// Create an Int8Array that is the copy of the firstHalfView array

var copiedView = new Int8Array(firstHalfView);

// Create an Int8Array using elements from an Array object

var ids = new Int8Array([10, 20, 30]);

所有类型化数组对象都具有以下属性:

  • length:数组中元素的个数
  • byteLength:数组的长度,以字节为单位
  • buffer:类型化数组使用的底层ArrayBuffer对象的引用
  • byteOffset:从其ArrayBuffer开始的偏移量,以字节为单位

一旦创建了类型化数组,就可以将其作为简单的数组对象使用。您可以使用括号符号和索引来设置和读取它的元素。如果设置的值不是类型化数组类型的类型,则该值会被适当地转换。例如,将 23.56 设置为一个Int8Array中的一个元素会将 23 设置为值。下面的代码演示如何读写类型化数组的内容:

// Create an Int8Array of 3 elements. Each element is an 8-bit sign integer.

var ids = new Int8Array(3);

// Populate the array

ids[0] = 10;

ids[1] = 20.89; // 20 will be stored

ids[2] = 140;   // -116 is stored as byte's range is -128 to 127.

// Read the elements

print("ids[0] = " + ids[0]);

print("ids[1] = " + ids[1]);

print("ids[2] = " + ids[2]);

ids[0] = 10

ids[1] = 20

ids[2] = -116

下面的代码从一个Array对象创建一个Int32Array,并读取所有元素:

// An Array object

var ids = [10, 20, 30];

// Create an Int32Array from ids

var typedIds = new Int32Array(ids);

// Read elements from typedids

for(var i = 0, len = typedIds.length; i < len; i++) {

print("typedIds[" + i + "] = " + typedIds[i]);

}

typedIds[0] = 10

typedIds[1] = 20

typedIds[2] = 30

也可以使用相同的ArrayBuffer来存储不同类型的值。回想一下,ArrayBuffer上的视图是打印出来的,而不是ArrayBuffer本身;它只包含原始的二进制数据。下面的代码创建了一个 8 字节的ArrayBuffer,在前 4 个字节中存储一个 32 位有符号整数,在后 4 个字节中存储一个 32 位有符号浮点数:

// Create an 8=byte buffer

var buffer = new ArrayBuffer(8);

// Create an Int32Array view for the first 4 bytes. byteOffset is 0

// and element count is 1

var id = new Int32Array(buffer, 0, 1);

// Create a Float32Array view for the second 4 bytes. The first 4 bytes

// will be used for integer value. byteOffset is 4 and element count is 1

var salary = new Float32Array(buffer, 4, 1);

// Use the Int32Array view to store an integer

id[0] = 1001;

// Use the Float32Array view to store a floating-point number

salary[0] = 129019.50;

// Read and print the two values using the two views

print("id = " + id[0]);

print("salary = " + salary[0]);

id = 1001

salary = 129019.5

当通过提供ArrayBuffer的长度来创建类型化数组时,ArrayBuffer的长度或大小必须是元素大小的倍数。例如,当您创建一个Int32Array时,其缓冲区的大小必须是 4 的倍数(32 位等于 4 字节)。以下代码将引发异常:

var buffer = new ArrayBuffer(15);

// Throws an exception because an element of Int32Array takes 4 bytes and

// 15, which is the buffer size for the view, is not a multiple of 4

var id = new Int32Array(buffer);

以下代码显示了如何使用slice()方法复制ArrayBuffer的一部分,并在复制的缓冲区上创建一个新视图:

// Create an ArrayBuffer of 4 bytes

var buffer = new ArrayBuffer(4);

// Create an Int8Array from buffer

var int8View1 = new Int8Array(buffer);

// Populate the array

int8View1[0] = 10;

int8View1[1] = 20;

int8View1[2] = 30;

int8View1[3] = 40;

print("In original buffer:")

for(var i = 0; i < int8View1.length; i++) {

print(int8View1[i]);

}

// Copy the last two bytes from buffer to buffer2

var buffer2 = buffer.slice(2, 4);

// Create an Int8Array from buffer2

var int8View2 = new Int8Array(buffer2);

print("In copied buffer:");

for(var i = 0; i < int8View2.length; i++) {

print(int8View2[i]);

}

In original buffer:

10

20

30

40

In copied buffer:

30

40

因为类型化数组是类似数组的对象,所以您可以在类型化数组上使用Array对象的大多数方法。下面的代码展示了如何使用join()方法连接Int32Array的元素:

// Create an Int32Array of 4 elements

var ids = new Int32Array(4);

// Populate the array

ids[0] = 101;

ids[1] = 102;

ids[2] = 103;

ids[3] = 104;

// Call the join() method of the Array.prototype object and print the result

var idsList = Array.prototype.join.call(ids);

print(idsList);

101,102,103,104

数据视图视图

DataView视图提供了从ArrayBuffer中读取和写入不同类型数据的底层接口。通常,当一个ArrayBuffer中的数据包含同一个ArrayBuffer的不同区域中不同类型的数据时,您会使用一个DataView。注意DataView不是一个类型化数组;它只是一个ArrayBuffer的视图。DataView构造函数的签名是:

DataView(arrayBuffer, byteOffset, byteLength)

它从byteOffset索引中引用byteLength字节,创建指定arrayBuffer的视图。如果byteLength未指定,则引用从byteOffset开始到结束的arrayBuffer。如果byteOffsetbyteLength都未指定,则引用入口arrayBuffer

与类型化数组视图类型不同,DataView对象可以使用混合数据类型值,因此它没有length属性。像类型化数组一样,它有bufferbyteOffsetbyteLength属性。它包含以下 getter 和 setter 方法,用于读取和写入不同类型的值:

  • getInt8(byteOffset)
  • getUint8(byteOffset)
  • getInt16(byteOffset, littleEndian)
  • getUint16(byteOffset, littleEndian)
  • getInt32(byteOffset, littleEndian)
  • getUint32(byteOffset, littleEndian)
  • getFloat32(byteOffset, littleEndian)
  • getFloat64(byteOffset, littleEndian)
  • setInt8(byteOffset, value)
  • setUint8(byteOffset, value)
  • setInt16(byteOffset, value, littleEndian)
  • setUint16(byteOffset, value, littleEndian)
  • setInt32(byteOffset, value, littleEndian)
  • setUint32(byteOffset, value, littleEndian)
  • setFloat32(byteOffset, value, littleEndian)
  • setFloat64(byteOffset, value, littleEndian)

多字节值类型的 getters 和 setters 有一个名为littleEndian的布尔型可选参数。它指定正在读取和设置的值是小端还是大端格式,如果未指定,则该值被假定为大端格式。如果您读取的数据来自不同的来源,并且具有不同的字节顺序,这将非常有用。下面的代码使用一个DataView从一个ArrayBuffer中读写一个 32 位有符号整数和一个 32 位浮点数:

// Create an ArrayBuffer of 8 bytes

var buffer = new ArrayBuffer(8);

// Create a DataView from the ArrayBuffer

var data = new DataView(buffer);

// Use the first 4 bytes to store a 32-bit signed integer

data.setInt32(0, 1001);

// Use the second 4 bytes to store a 32-bit floating-point number

data.setFloat32(4, 129019.50);

var id = data.getInt32(0);

var salary = data.getFloat32(4);

print("id = " + id);

print("salary = " + salary);

id = 1001

salary = 129019.5

使用列表、地图和集合

Nashorn 不提供内置对象来表示通用地图和集合。您可以将 Nashorn 中的任何对象用作键为字符串的地图。可以在 Nashorn 中创建对象来表示地图和集合。然而,这样做就像重新发明轮子一样。Java 编程语言提供了许多类型的集合,包括映射和集合。您可以直接在 Nashorn 中使用这些集合。关于如何在 Nashorn 脚本中使用 Java 类的更多细节,请参考Chapter 6。我将在这一节讨论在 Nashorn 中受到特殊对待的 Java ListMap

使用 Java 列表作为 Nashorn 数组

Nashorn 允许您将 Java List视为 Nashorn 数组来读取和更新List的元素。注意,它不允许您使用数组索引向List添加元素。您可以使用索引来访问和更新List的元素。Nashorn 为 Java List的实例添加了一个length属性,因此您可以将List视为一个类似数组的对象。清单 7-4 展示了一个在 Nashorn 脚本中使用 Java List的例子。

清单 7-4。在 Nashorn 脚本中将 java.util.List 作为数组对象进行访问和更新

// list.js

var ArrayList = Java.type("java.util.ArrayList");

var nameList = new ArrayList();

// Add few names using List.add() Java method

nameList.add("Lu");

nameList.add("Do");

nameList.add("Yu");

// Print the List

print("After adding names:");

for(var i = 0, len = nameList.size(); i < len; i++) {

print(nameList.get(i));

}

// Update names using array indexes

nameList[0] = "Su";

nameList[1] = "So";

nameList[2] = "Bo";

// The following statement will throw an IndexOutOfBoundsException because

// it is trying to add a new element using teh array syntax. You can only

// update an element, not add a new element, using the array syntax.

// nameList[3] = "An";

print("After updating names:");

for(var i = 0, len = nameList.length; i < len; i++) {

print(nameList[i]);

}

// Sort the list in natural order

nameList.sort(null);

// Use the Array.prototype.forEach() method to print the list

print("After sorting names:");

Array.prototype.forEach.call(nameList, function (value, index, data) {

print(value);

});

After adding names:

Lu

Do

Yu

After updating names:

Su

So

Bo

After sorting names:

Bo

So

Su

该示例执行几项操作:

  • 创建一个java.util.ArrayList的实例
  • 使用 Java List接口的add()方法向列表中添加三个元素
  • 使用List接口的size()get()方法打印元素
  • 使用用于访问数组元素的 Nashorn 语法更新元素并打印它们。它使用 Nashorn 属性length来获取列表的大小
  • 使用 Java List.sort()方法按照自然顺序对元素进行排序
  • 使用Array.prototype.forEach()方法打印排序后的元素

使用 Java 地图作为 Nashorn 对象

您已经看到 Nashorn 对象只是存储字符串-对象对的映射。Nashorn 允许使用 Java Map作为 Nashorn 对象,其中你可以使用Map中的键作为对象的属性。清单 7-5 显示了如何使用 Java Map作为 Nashorn 对象。

清单 7-5。使用 Java 地图作为 Nashorn 对象

// map.js

// Create a Map instance

var HashMap = Java.type("java.util.HashMap");

var map = new HashMap();

// Add key-value pairs to the map using Java methods

map.put("Li", "999-11-0001");

map.put("Su", "999-11-0002");

// You can treat the Map as an object and add key-value pairs

// as if you are adding proeprties to the object

map["Yu"] = "999-11-0003";

map["Do"] = "999-11-0004";

// Access values using the Java Map.get() method and the bracket notation

var liPhone = map.get("Li");  // Java way

var suPhone = map.get("Su");  // Java way

var yuPhone = map["Yu"];      // Nashorn way

var doPhone = map["Do"];      // Nashorn way

print("Li's Phone: " + liPhone);

print("su's Phone: " + suPhone);

print("Yu's Phone: " + yuPhone);

print("Do's Phone: " + doPhone);

Li's Phone: 999-11-0001

su's Phone: 999-11-0002

Yu's Phone: 999-11-0003

Do's Phone: 999-11-0004

使用 Java 数组

您可以在 Nashorn 脚本中使用 Java 数组。在 Rhino 和 Nashorn 中,用 JavaScript 创建 Java 数组的方式是不同的。在 Rhino 中,你需要使用java.lang.reflect.Array类的newInstance()静态方法创建一个 Java 数组,这种方法效率很低,也很有限。Nashorn 支持一种创建 Java 数组的新方法,我将很快讨论这种方法。Nashorn 也支持这种语法。以下代码显示了如何使用 Rhino 语法创建和访问 Java 数组:

// Create a java.lang.String array of 2 elements, populate it, and print the

// elements. In Rhino, you were able to use java.lang.String as the first

// argument; in Nashorn, you need to use java.lang.String.class instead.

var strArray = java.lang.reflect.Array.newInstance(java.lang.String.class, 2);

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

要创建原始类型数组,如数组intdouble等,您需要将它们的TYPE常量用于它们对应的包装类,如下所示:

// Create an int array of 2 elements, populate it, and print the elements

var intArray = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Nashorn 支持创建 Java 数组的新语法。首先,使用Java.type()方法创建适当的 Java 数组类型,然后使用熟悉的new操作符创建数组。下面的代码展示了如何在 Nashorn 中创建两个元素的String[]:

// Get the java.lang.String[] type

var StringArray = Java.type("java.lang.String[]");

// Create a String[] array of 2 elements

var strArray = new StringArray(2);

// Populate the array

strArray[0] = "Hello";

strArray[1] = "Array";

for(var i = 0; i < strArray.length; i++) {

print(strArray[i]);

}

Hello

Array

Nashorn 支持以同样的方式创建原始类型的数组。以下代码在 Nashorn 中创建两个元素的int[]:

// Get the int[] type

var IntArray = Java.type("int[]");

// Create a int[] array of 2 elements

var intArray = new IntArray(2);

intArray[0] = 100;

intArray[1] = 200;

for(var i = 0; i < intArray.length; i++) {

print(intArray[i]);

}

100

200

Tip

如果你想在 Nashorn 中创建多维 Java 数组,数组类型将会是你在 Java 中声明的数组。比如,Java.type("int[][]")会导入一个 Java int[][]数组类型;在导入的类型上使用 new 操作符将创建int[][]数组。

当需要 Java 数组时,可以使用 Nashorn 数组。Nashorn 将执行必要的转换。假设你有一个PrintArray类,如清单 7-6 所示,它包含一个接受String数组作为参数的print()方法。

清单 7-6。PrintArray 类

// PrintArray.java

package com.jdojo.script;

public class PrintArray {

public void print(String[] list) {

System.out.println("Inside print(String[] list):" + list.length);

for(String s : list) {

System.out.println(s);

}

}

}

以下脚本将一个 Nashorn 数组传递给PrintArray.print(String[])方法。Nashorn 负责将本机数组转换成一个String数组,如输出所示:

// Create a JavaScript array and populate it with three strings

var names = new Array();

names[0] = "Rhino";

names[1] = "Nashorn";

names[2] = "JRuby";

// Create an object of the PrintArray class

var PrintArray = Java.type("com.jdojo.script.PrintArray");

var pa = new PrintArray();

// Pass a JavaScript array to the PrintArray.print(String[] list) method

pa.print(names);

Inside print(String[] list):3

Rhino

Nashorn

JRuby

Nashorn 使用Java.to()Java.from()方法支持 Java 和 Nashorn 数组之间的数组类型转换。Java.to()方法将 Nashorn 数组类型转换为 Java 数组类型;它将 array 对象作为第一个参数,将目标 Java 数组类型作为第二个参数。目标数组类型可以指定为字符串或类型对象。下面的代码片段将 Nashorn 数组转换成 Java String[]:

// Create a JavaScript array and populate it with three integers

var personIds = [100, 200, 300];

// Convert the JavaScript integer array to Java String[]

var JavaStringArray = Java.to(personIds, "java.lang.String[]")

如果省略Java.to()函数中的第二个参数,Nashorn 数组将被转换为 Java Object[]

方法将 Java 数组类型转换成 Nashorn 数组。该方法将 Java 数组作为参数。以下代码片段显示了如何将 Java int[]转换为 JavaScript 数组:

// Create a Java int[]

var IntArray = Java.type("int[]");

var personIds = new IntArray(3);

personIds[0] = 100;

personIds[1] = 200;

personIds[2] = 300;

// Convert the Java int[] array to a Nashorn array

var jsArray = Java.from(personIds);

// Print the elements in the Nashorn array

for(var i = 0; i < jsArray.length; i++) {

print(jsArray[i]);

}

100

200

300

Tip

从一个 Nashorn 函数返回一个 Nashorn 数组到 Java 代码是可能的。您需要在 Java 代码中提取原生数组的元素,因此,您需要在 Java 中使用特定于 Nashorn 的类。不建议使用这种方法。您应该将 Nashorn 数组转换为 Java 数组,并从 Nashorn 函数返回 Java 数组,这样 Java 代码只处理 Java 类。

数组到 Java 集合的转换

只要有可能,Nashorn 就提供从 Nashorn 数组到 Java 数组、List s 和Map s 的自动转换。当 Nashorn 数组被转换成 Java Map时,数组元素的索引成为Map中的键,元素的值成为相应索引的值。考虑一个名为ArrayConversion的 Java 类,如清单 7-7 所示。它包含四个接受数组、List s 和Map s 作为参数的方法。

清单 7-7。一个名为 ArrayConversion 的 Java 类

// ArrayConversion.java

package com.jdojo.script;

import java.util.Arrays;

import java.util.List;

import java.util.Map;

public class ArrayConversion {

public static void printInts(int[] ids) {

System.out.print("Inside printInts():");

System.out.println(Arrays.toString(ids));

}

public static void printDoubles(double[] salaries) {

System.out.print("Inside printDoubles(double[]):");

System.out.println(Arrays.toString(salaries));

}

public static void printList(List<Integer> idsList) {

System.out.print("Inside printList():");

System.out.println(idsList);

}

public static void printMap(Map<?,?> phoneMap) {

System.out.println("Inside printMap():");

phoneMap.forEach((key, value) -> {

System.out.println("key = " + key + ", value = " + value);

});

}

}

考虑清单 7-8 所示的 Nashorn 脚本。它将 Nashorn 数组传递给期望数组、List s 和Map s 的 Java 方法。输出证明所有方法都被调用,并且 Nashorn 运行时提供自动转换。请注意,当 Nashorn 数组包含与 Java 类型不匹配的类型元素时,这些元素会根据 Nashorn 类型转换规则转换为适当的 Java 类型。例如,Nashorn 中的一个字符串被转换为 Java int中的 0。

清单 7-8。测试 ArrayConversion 类的 Nashorn 脚本

// arrayconversion.js

var ArrayConversion = Java.type("com.jdojo.script.ArrayConversion");

ArrayConversion.printInts([1, 2, 3]);

ArrayConversion.printInts([1, "Hello", 3]); // "hello" is converted to 0

// Non-integers will be converted to corresponding integers per Nashorn rules

// when a Nashorn array is converted to a Java array. true and false are

// converted to 1 and 0, and 10.3 is converted to 10.

ArrayConversion.printInts([1, true, false, 10.3]);

// Nashorn array to Java double[] conversion

ArrayConversion.printDoubles([10.89, "Hello", 3]);

// Nashorn array to Java List conversion

ArrayConversion.printList([10.89, "Hello", 3]);

// Nashorn array to Java Map conversion

ArrayConversion.printMap([10.89, "Hello", 3]);

Inside printInts():[1, 2, 3]

Inside printInts():[1, 0, 3]

Inside printInts():[1, 1, 0, 10]

Inside printDoubles(double[]):[10.89, NaN, 3.0]

Inside printList():[10.89, Hello, 3]

Inside printMap():

key = 0, value = 10.89

key = 1, value = Hello

key = 2, value = 3

有时候,Nashorn 不可能自动地将 Nashorn 数组转换成 Java 数组——特别是当 Java 方法重载时。考虑下面引发异常的代码:

var Arrays = Java.type("java.util.Arrays");

var str = Arrays.toString([0, 1, 2]); // Throws a java.lang.NoSuchMethodException

Arrays.toString()方法被重载,Nashorn 无法决定调用哪个版本的方法。在这种情况下,您必须显式地将 Nashorn 数组转换为特定的 Java 数组类型,或者选择要调用的特定 Java 方法。以下代码显示了这两种方法:

var Arrays = Java.type("java.util.Arrays");

// Explicitly convert Nashorn array to Java int[]

var str1 = Arrays.toString(Java.to([1, 2, 3], "int[]"));

// Explicitly choose the Arrays.toString(int[]) method to call

var str2 = Arrays["toString(int[])"]([1, 2, 3]);

摘要

Nashorn 中的数组是一个专门的对象,称为Array对象,用于表示值的有序集合。一个Array对象以一种特殊的方式对待某些属性名。如果一个属性名可以转换成一个介于 0 和 2 32 -2(包括 0 和 2)之间的整数,这样的属性名称为数组索引,属性称为元素。一个Array对象有一个称为length的特殊属性,代表数组中元素的数量。

可以使用数组文字或使用Array对象的构造函数来创建数组。数组文字是用括号括起来的逗号分隔的表达式列表,组成数组的元素。Nashorn 中的数组是可变长度的,可以是密集的,也可以是稀疏的。数组中的元素也可以是不同的类型。

您可以使用括号符号和数组元素的索引来访问数组元素。您可以使用delete操作符删除数组元素。您可以使用for循环、for..in循环和for..each..in循环来迭代数组元素。for..infor..each..in循环遍历数组的元素和非元素属性。Array.prototype对象包含一个forEach()方法,该方法只迭代数组元素,并为每个元素回调传递的函数。

Array对象包含几个有用的方法来处理数组元素。如果指定的object是一个数组,Array.isArray(object)静态方法返回true;否则,它返回false。其他方法如concat()join()slice()splice()sort()等等都在Array.prototype对象中。

类似数组的对象是一个 Nashorn 对象,它有一个length属性和属性名,这些属性名是有效的数组索引。String 对象和arguments对象是类数组对象的例子。Array对象中的大多数方法都是通用的,它们可以用在任何类似数组的对象上。

Nashorn 支持类型化数组。类型化数组是包含原始二进制数据的ArrayBuffer对象的类型化视图。不同类型的数组是可用的,比如Int8Array处理 8 位有符号整数,Int32Array处理 32 位有符号整数,等等。一旦创建了类型化数组,就可以像数组一样使用它来处理特定类型的值。DataView对象提供了一个低级接口来处理一个ArrayBuffer对象,其二进制数据可能包含不同类型的数值。

所有 Nashorn 对象都是地图。Nashorn 允许您以一种特殊的方式对待 Java Map s,允许您使用带键的括号符号作为索引来访问它们的值。Nashorn 让你像对待 Nashorn 数组一样对待 Java。您可以使用括号符号读取和更新 Java List的元素,其中元素的索引被视为数组中的索引。Nashorn 为 Java List对象创建了一个名为length的特殊属性,表示List中元素的数量。如果在 Nashorn 中需要任何其他类型的集合,可以使用 Java 中相应的集合。

Nashorn 允许您在脚本中使用 Java 数组。您需要使用Java.type()方法在脚本中导入 Java 数组类型。比如说。Java.type("int[]")返回一个代表 Java int[]的 Java 类型。一旦获得了 Java 数组类型,就需要使用new操作符来创建数组。Nashorn 还支持 Nashorn 到 Java 和 Java 到 Nashorn 的数组转换。Java.to()方法将 Nashorn 数组转换成 Java 数组,而Java.from()方法将 Java 数组转换成 Nashorn 数组。Nashorn 尝试执行从 Nashorn 数组到 Java 数组、List s 和Map s 的自动转换,前提是类型转换的选择是唯一的。在其他情况下,您将需要执行从 Nashorn 数组到集合的显式转换。

八、实现脚本引擎

在本章中,您将学习:

  • 实现新脚本引擎时需要开发的脚本引擎组件
  • 如何实现一个简单脚本引擎的不同组件,该引擎将对两个数执行加、减、乘、除运算
  • 如何封装脚本引擎的代码
  • 如何部署和测试脚本引擎

介绍

实现一个成熟的脚本引擎不是一件简单的任务,这超出了本书的范围。本章旨在为您提供实现脚本引擎所需的设置的简要但完整的概述。在本节中,您将实现一个简单的脚本引擎,称为JKScript引擎。它将使用以下规则计算算术表达式:

  • 它将计算由两个操作数和一个运算符组成的算术表达式
  • 表达式可能有两个数字文字、两个变量,或者一个数字文字和一个变量作为操作数。数字文字必须是十进制格式。不支持十六进制、八进制和二进制数字文本
  • 表达式中的算术运算仅限于加、减、乘和除
  • 它会将+-*/识别为算术运算符
  • 引擎将返回一个Double对象作为表达式的结果
  • 可以使用引擎的全局范围或引擎范围绑定将表达式中的操作数传递给引擎
  • 它应该允许从一个String对象和一个java.io.Reader对象执行脚本。然而,一个Reader应该只有一个表达式作为它的内容
  • 它不会实现InvocableCompilable接口

使用这些规则,脚本引擎的一些有效表达式如下:

  • 10 + 90
  • 10.7 + 89.0
  • +10 + +90
  • num1 + num2
  • num1 * num2
  • 78.0 / 7.5

在实现脚本引擎时,您需要为以下两个接口提供实现:

  • javax.script.ScriptEngineFactory
  • javax.script.ScriptEngine

作为JKScript脚本引擎实现的一部分,你将开发表 8-1 中列出的三个类。在随后的部分中,您将开发这些类。

表 8-1。

The List of Classes to be Developed for the JKScript Script Engine

班级 描述
Expression Expression类是脚本引擎的核心。它执行解析和评估算术表达式的工作。它在JKScriptEngine类的eval()方法中使用。
JKScriptEngine 接口的一个实现。它扩展了实现ScriptEngine接口的AbstractScriptEngine类。AbstractScriptEngine类为ScriptEngine接口的eval()方法的几个版本提供了标准实现。您需要实现下面两个版本的eval()方法:Object eval(String, ScriptContext) Object eval(Reader, ScriptContext)
JKScriptEngineFactory 接口的一个实现。

表达式类

Expression类包含解析和评估算术表达式的主要逻辑。清单 8-1 包含了Expression类的完整代码。

清单 8-1。分析和计算算术表达式的表达式类

// Expression.java

package com.jdojo.script;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import javax.script.ScriptContext;

public class Expression {

private String exp;

private ScriptContext context;

private String op1;

private char op1Sign = '+';

private String op2;

private char op2Sign = '+';

private char operation;

private boolean parsed;

public Expression(String exp, ScriptContext context) {

if (exp == null || exp.trim().equals("")) {

throw new IllegalArgumentException(this.getErrorString());

}

this.exp = exp.trim();

if (context == null) {

throw new IllegalArgumentException(        "ScriptContext cannot be null.");

}

this.context = context;

}

public String getExpression() {

return exp;

}

public ScriptContext getScriptContext() {

return context;

}

public Double eval() {

// Parse the expression

if (!parsed) {

this.parse();

this.parsed = true;

}

// Extract the values for the operand

double op1Value = getOperandValue(op1Sign, op1);

double op2Value = getOperandValue(op2Sign, op2);

// Evaluate the expression

Double result = null;

switch (operation) {

case '+':

result = op1Value + op2Value;

break;

case '-':

result = op1Value - op2Value;

break;

case '*':

result = op1Value * op2Value;

break;

case '/':

result = op1Value / op2Value;

break;

default:

throw new RuntimeException(        "Invalid operation:" + operation);

}

return result;

}

private double getOperandValue(char sign, String operand) {

// Check if operand is a double

double value;

try {

value = Double.parseDouble(operand);

return sign == '-' ? -value : value;

}

catch (NumberFormatException e) {

// Ignore it. Operand is not in a format that can be

// converted to a double value.

}

// Check if operand is a bind variable

Object bindValue = context.getAttribute(operand);

if (bindValue == null) {

throw new RuntimeException(operand +         " is not found in the script context.");

}

if (bindValue instanceof Number) {

value = ((Number) bindValue).doubleValue();

return sign == '-' ? -value : value;

}

else {

throw new RuntimeException(operand +         " must be bound to a number.");

}

}

public void parse() {

// Supported expressions are of the form v1 op v2, // where v1 and v2 are variable names or numbers,

// and op could be +, -, *, or /

// Prepare the pattern for the expected expression

String operandSignPattern = "([+-]?)";

String operandPattern = "([\\p{Alnum}\\p{Sc}_.]+)";

String whileSpacePattern = "([\\s]*)";

String operationPattern = "([+*/-])";

String pattern = "^" + operandSignPattern + operandPattern +

whileSpacePattern + operationPattern + whileSpacePattern +

operandSignPattern + operandPattern + "$";

Pattern p = Pattern.compile(pattern);

Matcher m = p.matcher(exp);

if (!m.matches()) {

// The expression is not in the expected format

throw new IllegalArgumentException(this.getErrorString());

}

// Get operand-1

String temp = m.group(1);

if (temp != null && !temp.equals("")) {

this.op1Sign = temp.charAt(0);

}

this.op1 = m.group(2);

// Get operation

temp = m.group(4);

if (temp != null && !temp.equals("")) {

this.operation = temp.charAt(0);

}

// Get operand-2

temp = m.group(6);

if (temp != null && !temp.equals("")) {

this.op2Sign = temp.charAt(0);

}

this.op2 = m.group(7);

}

private String getErrorString() {

return "Invalid expression[" + exp + "]" +

"\nSupported expression syntax is: op1 operation op2" +

"\n where op1 and op2 can be a number or a bind variable" +

" , and operation can be +, -, *, and /.";

}

@Override

public String toString() {

return "Expression: " + this.exp + ", op1 Sign = " +

op1Sign + ", op1 = " + op1 + ", op2 Sign = " +

op2Sign + ", op2 = " + op2 + ", operation = " + operation;

}

}

Expression类被设计用来解析和评估以下形式的算术表达式:

op1 operation op2

这里,op1op2是两个操作数,可以是十进制格式的数字或变量,operation可以是+-*/

建议使用的Expression类是:

Expression exp = new Expression(expression, scriptContext);

Double value = exp.eval();

让我们详细讨论一下Expression类的重要组件。

实例变量

名为expcontext的实例变量分别是表达式和对表达式求值的ScriptContext。它们被传递给该类的构造函数。

名为op1op2的实例变量分别代表表达式中的第一个和第二个操作数。实例变量op1Signop2Sign分别代表表达式中第一个和第二个操作数的符号,可以是“+”或“-”。当使用parse()方法解析表达式时,操作数及其符号被填充。

名为operation的实例变量表示要对操作数执行的算术运算(+-*/)。

名为parsed的实例变量用于跟踪表达式是否被解析。parse()方法将其设置为true

构造函数

Expression类的构造函数接受一个表达式和一个ScriptContext。它确保它们不是null,并将它们存储在实例变量中。在将表达式存储到名为exp的实例变量中之前,它会从表达式中删除前导空格和尾随空格。

parse()方法

parse()方法将表达式解析成操作数和操作。它使用正则表达式来解析表达式文本。正则表达式要求表达式文本采用以下形式:

  • 第一个操作数的可选符号+-
  • 第一个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成
  • 任意数量的空格
  • 可能是+-*/的操作标志
  • 第二个操作数的可选符号+-
  • 第二个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成

正则表达式([+-]?)将匹配操作数的可选符号。正则表达式([\\p{Alnum}\\p{Sc}_.]+)会匹配一个操作数,可能是十进制数,也可能是名字。正则表达式([\\s]*)将匹配任意数量的空格。正则表达式([+*/-])将匹配一个操作符。所有正则表达式都用括号括起来形成组,这样就可以捕获表达式的匹配部分。

如果一个表达式匹配正则表达式,parse()方法将匹配部分存储到各自的实例变量中。

注意,匹配操作数的正则表达式并不完美。它将允许几种无效的情况,比如一个操作数有多个小数点,等等。但是,对于本演示来说,这就足够了。

getOperandValue()方法

在表达式被解析后,在表达式求值期间使用getOperandValue()方法。如果操作数是一个double数,它通过应用操作数的符号返回值。否则,它会在ScriptContext中查找操作数的名称。如果在ScriptContext中没有找到操作数的名称,它抛出一个RuntimeException。如果在ScriptContext中找到操作数的名称,它将检查该值是否为数字。如果该值是一个数字,则在将符号应用于该值后返回该值;否则抛出一个RuntimeException

方法不支持十六进制、八进制和二进制格式的操作数。例如,像“0x2A + 0b1011”这样的表达式将不会被视为具有两个带int文字的操作数的表达式。读者可以增强这种方法,以支持十六进制、八进制和二进制格式的数字文字。

eval()方法

eval()方法计算表达式并返回一个double值。首先,如果表达式还没有被解析,它就解析它。注意,多次调用eval()只会解析表达式一次。

它获取两个操作数的值,执行运算,并返回表达式的值。

JKScriptEngine 类

清单 8-2 包含了JKScript脚本引擎的实现。其eval(String, ScriptContext)方法包含主逻辑,如图所示:

Expression exp = new Expression(script, context);

Object result = exp.eval();

它创建了一个Expression类的对象。它调用评估表达式并返回结果的Expression对象的eval()方法。

eval(ReaderScriptContext)方法从Reader中读取所有行,将它们连接起来,并将结果String传递给eval(String, ScriptContext)方法来计算表达式。注意一个Reader必须只有一个表达式。一个表达式可以拆分成多行。Reader中的空白被忽略。

清单 8-2。JKScript 脚本引擎的实现

// JKScriptEngine.java

package com.jdojo.script;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.Reader;

import javax.script.AbstractScriptEngine;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.ScriptEngineFactory;

import javax.script.ScriptException;

import javax.script.SimpleBindings;

public class JKScriptEngine extends AbstractScriptEngine {

private ScriptEngineFactory factory;

public JKScriptEngine(ScriptEngineFactory factory) {

this.factory = factory;

}

@Override

public Object eval(String script, ScriptContext context)

throws ScriptException {

try {

Expression exp = new Expression(script, context);

Object result = exp.eval();

return result;

}

catch (Exception e) {

throw new ScriptException(e.getMessage());

}

}

@Override

public Object eval(Reader reader, ScriptContext context) throws ScriptException {

// Read all lines from the Reader

BufferedReader br = new BufferedReader(reader);

String script = "";

String str = null;

try {

while ((str = br.readLine()) != null) {

script = script + str;

}

}

catch (IOException e) {

throw new ScriptException(e);

}

// Use the String version of eval()

return eval(script, context);

}

@Override

public Bindings createBindings() {

return new SimpleBindings();

}

@Override

public ScriptEngineFactory getFactory() {

return factory;

}

}

JKScriptEngineFactory 类

清单 8-3 包含了JKScript引擎的ScriptEngineFactory接口的实现。它的一些方法返回一个"Not Implemented"字符串,因为你不支持这些方法公开的特性。JKScriptEngineFactory类中的代码是不言自明的。使用ScriptEngineManager可以获得一个JKScript引擎的实例,其名称为jksJKScriptjkscript,如getNames()方法中编码的那样。

清单 8-3。JKScript 脚本引擎的 ScriptEngineFactory 实现

// JKScriptEngineFactory.java

package com.jdojo.script;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collections;

import java.util.List;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineFactory;

public class JKScriptEngineFactory implements ScriptEngineFactory {

@Override

public String getEngineName() {

return "JKScript Engine";

}

@Override

public String getEngineVersion() {

return "1.0";

}

@Override

public List<String> getExtensions() {

return Collections.unmodifiableList(Arrays.asList("jks"));

}

@Override

public List<String> getMimeTypes() {

return Collections.unmodifiableList(Arrays.asList("text/jkscript") );

}

@Override

public List<String> getNames() {

List<String> names = new ArrayList<>();

names.add("jks");

names.add("JKScript");

names.add("jkscript");

return Collections.unmodifiableList(names);

}

@Override

public String getLanguageName() {

return "JKScript";

}

@Override

public String getLanguageVersion() {

return "1.0";

}

@Override

public Object getParameter(String key) {

switch (key) {

case ScriptEngine.ENGINE:

return getEngineName();

case ScriptEngine.ENGINE_VERSION:

return getEngineVersion();

case ScriptEngine.NAME:

return getEngineName();

case ScriptEngine.LANGUAGE:

return getLanguageName();

case ScriptEngine.LANGUAGE_VERSION:

return getLanguageVersion();

case "THREADING":

return "MULTITHREADED";

default:

return null;

}

}

@Override

public String getMethodCallSyntax(String obj, String m, String[] p) {

return "Not implemented";

}

@Override

public String getOutputStatement(String toDisplay) {

return "Not implemented";

}

@Override

public String getProgram(String[] statements) {

return "Not implemented";

}

@Override

public ScriptEngine getScriptEngine() {

return new JKScriptEngine(this);

}

}

准备部署

在为JKScript脚本引擎打包类之前,您需要再执行一个步骤:创建一个名为META-INF的目录。在META-INF目录下,创建一个名为services的子目录。在services目录下,创建一个名为javax.script.ScriptEngineFactory的文本文件。请注意,文件名必须是所提到的名称,并且不应该有任何扩展名,例如.txt

编辑javax.script.ScriptEngineFactory文件并输入如清单 8-4 所示的内容。文件中的第一行是以#号开头的注释。第二行是JKScript脚本引擎工厂类的完全限定名。

清单 8-4。名为 javax . script . scriptenginefactory 的文件的内容

#The factory class for the JKScript engine

com.jdojo.script.JKScriptEngineFactory

为什么一定要执行这一步?您将把javax.script.ScriptEngineFactory文件和JKScript引擎的类文件打包在一个 JAR 文件中。脚本引擎的发现机制在类路径的所有 JAR 文件的META-INF/services目录中搜索这个文件。如果找到该文件,将读取其内容,并且该文件中的所有脚本工厂类都将被实例化并包含在脚本引擎工厂列表中。因此,这一步对于让你的JKScript引擎被ScriptEngineManager自动发现是必要的。

打包 jscript 文件

您需要将JKScript脚本引擎的所有文件打包到一个名为jkscript.jar的 JAR 文件中。您也可以将该文件命名为任何其他名称。以下是文件及其目录的列表。请注意,在这种情况下,一个空的manifest.mf文件将起作用:

  • com\jdojo\script\Expression.class
  • com\jdojo\script\JKScriptEngine.class
  • com\jdojo\script\JKScriptEngineFactory.class
  • META-INF\manifest.mf
  • META-INF\services\javax.script.ScriptEngineFactory

您可以手动创建jkscript.jar文件,方法是将除了manifest.mf文件之外的所有这些文件复制到一个目录中,比如 Windows 上的C:\build,然后从C:\build目录执行以下命令:

C:\build> jar cf jkscript.jar com\jdojo\script\*.class META-INF\services\*.*

在本书的可下载包的src\JavaScripts目录中可以找到jkscript.jar文件。可下载的源代码包含一个 NetBeans 8.0 项目,并且jkscript.jar文件被添加到项目的类路径中。如果从附带的 NetBeans IDE 中运行引擎,则不需要执行打包和部署步骤来使用JKScript引擎。

使用 jscript 脚本引擎

是时候测试你的JKScript脚本引擎了。第一步也是最重要的一步是将您在上一节中创建的jkscript.jar包含到应用类路径中。一旦在应用类路径中包含了jkscript.jar文件,使用JKScript与使用任何其他脚本引擎没有什么不同。

下面的代码片段创建了一个使用JKScript作为名称的JKScript脚本引擎的实例。您也可以使用它的其他名称,如jksjkscript:

// Create the JKScript engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JKScript");

if (engine == null) {

System.out.println("JKScript engine is not available. " +

"Add jkscript.jar to CLASSPATH.");

}

else {

// Evaluate your JKScript

}

清单 8-5 包含一个程序,它使用JKScript脚本引擎来评估不同类型的表达式。执行存储在String对象和文件中的表达式。一些表达式使用数值和一些绑定变量,它们的值在引擎范围和引擎的默认ScriptContext的全局范围内的绑定中传递。注意,这个程序期望在当前目录中有一个名为jkscript.txt的文件,其中包含一个可以被JKScript脚本引擎理解的算术表达式。如果脚本文件不存在,程序将在标准输出中打印一条消息,其中包含预期脚本文件的路径。您可能会在最后一行得到不同的输出。

清单 8-5。使用 JKScript 脚本引擎

// JKScriptTest.java

package com.jdojo.script;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.Reader;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class JKScriptTest {

public static void main(String[] args) throws FileNotFoundException, IOException {

// Create JKScript engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JKScript");

if (engine == null) {

System.out.println("JKScript engine is not available. " +

"Add jkscript.jar to CLASSPATH.");

return;

}

// Test scripts as String

testString(manager, engine);

// Test scripts as a Reader

testReader(manager, engine);

}

public static void testString(ScriptEngineManager manager, ScriptEngine engine) {

try {

// Use simple expressions with numeric literals

String script = "12.8 + 15.2";

Object result = engine.eval(script);

System.out.println(script + " = " + result);

script = "-90.0 - -10.5";

result = engine.eval(script);

System.out.println(script + " = " + result);

script = "5 * 12";

result = engine.eval(script);

System.out.println(script + " = " + result);

script = "56.0 / -7.0";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Use global scope bindings variables

manager.put("num1", 10.0);

manager.put("num2", 20.0);

script = "num1 + num2";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Use global and engine scopes bindings. num1 from

// engine scope and num2 from global scope will be used.

engine.put("num1", 70.0);

script = "num1 + num2";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Try mixture of number literal and bindings. num1 // from the engine scope bindings will be used

script = "10 + num1";

result = engine.eval(script);

System.out.println(script + " = " + result);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

public static void testReader(ScriptEngineManager manager, ScriptEngine engine) {

try {

Path scriptPath = Paths.get("jkscript.txt").toAbsolutePath();

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath +

" script file does not exist.");

return;

}

try(Reader reader = Files.newBufferedReader(scriptPath);) {

Object result = engine.eval(reader);

System.out.println("Result of " +

scriptPath + " = " + result);

}

}

catch(ScriptException | IOException e) {

e.printStackTrace();

}

}

}

12.8 + 15.2 = 28.0

-90.0 - -10.5 = -79.5

5 * 12 = 60.0

56.0 / -7.0 = -8.0

num1 + num2 = 30.0

num1 + num2 = 90.0

10 + num1 = 80. 0

Result of C:\jkscript.txt = 190.0

摘要

您可以使用 Java Script API 实现脚本引擎。您需要为ScriptEngineScriptEngineFactory接口提供实现。您需要以某种方式打包您的脚本引擎代码,这样引擎就可以在运行时被ScriptManager发现。引擎的 JAR 文件应该包含一个名为META-INF\services\javax.script.ScriptEngineFactory的文件,该文件应该包含所有脚本引擎工厂类的全限定名称;Java Script API 会自动发现这些脚本引擎工厂。一旦打包并部署了脚本引擎代码,就可以像访问 Nashorn 和其他脚本引擎一样访问它。

九、jrunscript命令行 Shell

在本章中,您将学习:

  • 什么是jrunscript命令行 shell
  • 如何调用jrunscript命令行 shell
  • 调用jrunscript命令行外壳的不同模式
  • 如何用jrunscript命令行 shell 列出可用的脚本引擎
  • 如何向jrunscript命令行外壳添加脚本引擎
  • 如何向jrunscript命令行 shell 传递参数
  • jrunscript命令行 shell 提供的全局函数

JDK 包括一个名为jrunscript的命令行脚本外壳。它独立于脚本引擎,可以用来评估任何脚本,包括你在第八章的中开发的JKScript。您可以在JAVA_HOME\bin目录中找到这个 shell,其中JAVA_HOME是您安装 JDK 的目录。在这一节中,我将讨论如何使用jrunscript shell 来评估使用不同脚本引擎的脚本。

语法

使用jrunscript shell 的语法是:

jrunscript [options] [arguments]

[options][arguments]都是可选的。但是,如果两者都指定了,[options]必须在[arguments]之前。表 9-1 列出了jrunscript外壳的所有可用选项。

表 9-1。

The List of Options for the jrunscript Shell

[计]选项 描述
-classpath <path> 用于指定类路径。
-cp <path> -classpath选项相同。
-D<name>=<value> 为 Java 运行时设置系统属性。
-J<flag> 将指定的<flag>传递给运行jrunscript的 JVM。
-l <language> 允许您指定一种您想与jrunscript一起使用的脚本语言。默认情况下,Rhino JavaScript 用于 JDK 6 和 JDK 7。在 JDK 8 中,Nashorn 是默认设置。如果您想使用 JavaScript 之外的语言,比如说JKScript,您将需要使用-cp-classpath选项来包含包含脚本引擎的 JAR 文件。
-e <script> 执行指定的脚本。通常,它用于执行一行脚本。
-encoding <encoding> 指定读取脚本文件时使用的字符编码。
-f <script-file> 以批处理模式评估指定的script-file
-f - 允许您在交互模式下评估脚本。它从标准输入中读取脚本并执行它。
-help 输出帮助消息并退出。
-? 输出帮助消息并退出。
-q 列出所有可用的脚本引擎和出口。请注意,除了 JavaScript 之外的脚本引擎只有在您使用-cp-classpath选项包含它们的 JAR 文件时才可用。

命令的[arguments]部分是一个参数列表,根据是否使用了-e-f选项来解释。传递给脚本的参数在脚本中作为一个名为arguments的对象存在。关于在脚本中使用arguments对象的更多细节,请参考第四章。表 9-2 列出了与-e-f选项一起使用时参数的解释。

表 9-2。

Interpretation of [arguments] in Combination of the -e or -f Option

-e 或-f 选项 争论 解释
Yes Yes 如果指定了-e-f选项,所有参数都将作为脚本参数传递给脚本。
No Yes 如果参数没有指定-e-f选项,第一个参数被认为是要运行的脚本文件。其余的参数(如果有)作为脚本参数传递给脚本。
No No 如果缺少参数和-e-f选项,shell 将在交互模式下工作,以交互方式执行在标准输入中输入的脚本。

外壳的执行模式

您可以在以下三种模式下使用jrunscript shell:

  • 单行模式
  • 成批处理方式
  • 对话方式

单行模式

-e选项允许您在一行模式下使用 shell。它执行一行脚本。以下命令使用 Nashorn 引擎在标准输出上打印一条消息:

C:\>jrunscript -e "print('Hello Nashorn!');"

Hello Nashorn!

C:\>

在单行模式下,整个脚本必须在一行中输入。但是,一行脚本可能包含多条语句。

成批处理方式

-f选项允许您在批处理模式下使用 shell。它执行一个脚本文件。考虑一个名为jrunscripttest.js的脚本文件,如清单 9-1 所示。

清单 9-1。用 Nashorn JavaScript 编写的 jrunscripttest.js 脚本文件

// jrunscripttest.js

// Print a message

print("Hello Nashorn!");

// Add two integers and print the value

var x = 10;

var y = 20;

var z = x + y;

printf("x + y = z", x, y, z);

以下命令以批处理模式运行jrunscripttest.js文件中的脚本。如果jrunscripttest.js文件不在当前目录中,您可能需要指定它的完整路径。

C:\>jrunscript -f jrunscripttest.js

Hello Nashorn!

10 + 20 = 30

C:\>

对话方式

在交互模式下,shell 读取并评估在标准输入上输入的脚本。有两种方法可以在交互模式下使用 shell:

  • 不使用-e-f选项,也不使用参数
  • 使用“-f -”选项

以下命令不使用任何选项和参数来进入交互模式。按 Enter 键会让 shell 评估输入的脚本。请注意,您需要执行exit()quit()功能来退出交互模式:

c:\>jrunscript

nashorn> print("Hello Interactive mode!");

Hello Interactive mode!

nashorn> var num = 190;

nashorn> print("num is " + num);

num is 190

nashorn> exit();

C:\>

列出可用的脚本引擎

jrunscript shell 是一个脚本语言中立的 shell。您可以使用它来运行脚本引擎 JAR 文件可用的任何脚本语言的脚本。缺省情况下,Nashorn JavaScript 引擎是可用的。要列出所有可用的脚本引擎,请使用如下所示的-q选项:

c:\>jrunscript -q

Language ECMAScript ECMA - 262 Edition 5.1 implementation "Oracle Nashorn" 1.8.0_05

请参考下一节如何将脚本引擎添加到 shell 中。

向外壳添加脚本引擎

如何让脚本引擎而不是 Nashorn 引擎对 shell 可用?要使脚本引擎对 shell 可用,您需要使用-classpath-cp选项为脚本引擎提供 JAR 文件列表。以下命令通过为JythonJKScript引擎提供 JAR 文件列表,使JKScriptjython脚本引擎对 shell 可用。请注意,默认情况下,Nashorn 引擎始终可用。该命令使用-q选项列出所有可用的脚本引擎:

c:\> jrunscript -cp C:\jython-standalone-2.5.3.jar;C:\jkscript.jar -q

Language python 2.5 implementation "jython" 2.5.3

Language ECMAScript ECMA - 262 Edition 5.1 implementation "Oracle Nashorn" 1.8.0_05

Language JKScript 1.0 implementation "JKScript Engine" 1.0

Tip

使用-cp-classpath选项设置的类路径仅对使用该选项的命令有效。如果在交互模式下运行 shell,则类路径对整个交互会话都有效。

使用其他脚本引擎

您可以通过使用-l选项指定脚本引擎名称来使用其他脚本引擎。您必须使用-cp-classpath选项为脚本引擎指定 JAR 文件,这样 shell 就可以访问引擎。以下命令在交互模式下使用JKScript引擎:

C:\>jrunscript -cp C:\jkscript.jar -l JKScript

jks> 10 + 30

40.0

jks> +89.7 + -9.7

80.0

jks>

向脚本传递参数

jrunscript shell 允许向脚本传递参数。这些参数在一个名为arguments的类似数组的对象中对脚本可用。您可以用特定于语言的方式访问脚本中的arguments数组。以下命令传递三个参数 10、20 和 30,并打印第一个参数的值:

C:\>jrunscript -e "print('First argument is ' + arguments[0])" 10 20 30

First argument is 10

考虑清单 9-2 所示的 Nashorn JavaScript 文件nashornargstest.js,它打印了传递给脚本的参数数量及其值。

清单 9-2。用 Nashorn JavaScript 编写的 nashornargstest.js 文件,用于打印命令行参数

// nashornargstest.js

print("Number of arguments:" + arguments.length);

print("Arguments are ") ;

for(var i = 0; i < arguments.length; i++) {

print(arguments[i]);

}

以下命令使用jrunscript shell 运行nashornargstest.js文件:

C:\>jrunscript nashornargstest.js

Number of arguments:0

Arguments are

C:\>jrunscript nashornargstest.js 10 20 30

Number of arguments:3

Arguments are

10

20

30

如果您想从 Java 应用运行nashornargstest.js文件,您需要向引擎传递一个名为arguments的参数。名为arguments的参数由 shell 自动传递给脚本,而不是由 Java 应用传递。

全局函数

jrunscript命令行 shell 使几个全局函数可供使用,如表 9-3 中所列。

表 9-3。

The List of Global Objects Loaded by the jrunscript Command-Line Shell

功能 描述
cat(path, pattern) 显示由path指定的文件、URL 或 InputStream 的内容。或者,您可以指定pattern只显示匹配的内容。
cd(target) 将当前工作目录更改为target目录。
cp(from, to) 将文件、URL 或流复制到另一个文件或流。
date() 使用当前区域设置打印当前日期。
del(pathname) rm命令的同义词。
dir(d, filter) ls命令的同义词。
dirname(pathname) 返回指定pathname的目录部分。
echo(str) 回显指定的字符串参数。
exec(cmd) 启动子进程,执行指定的命令,等待完成,并返回退出代码。
exit(code) 用指定的code作为退出代码退出 shell 程序。
find(dir, pattern, callback) dir中查找文件名与指定的pattern匹配的文件。当找到匹配时,调用callback函数传递找到的文件。搜索在所有子目录中递归执行。您可以将此表中列出的一些函数作为callback进行传递。如果没有指定callback,默认是打印找到的文件路径。如果未指定pattern,则打印所有文件。
grep(pattern, files) 类似 Unix 的grep,但是接受 JavaScript regex 模式。
ip(name) 打印给定域名的 IP 地址。
load(path) 从流、文件或 URL 加载并计算 JavaScript 代码。
ls(dir, filter) 列出dir中与filter正则表达式匹配的文件。
mkdir(dir) 创建一个名为dir的新目录。
mkdirs(dir) 创建一个名为dir的目录,包括任何必要但不存在的父目录。
mv(from, to) 将文件移动到另一个目录。
printf(format, args) 一个类似 C 的 printf。
pwd() 打印工作目录。
quit(code) exit(code)的同义词。
read(prompt, multiline) 打印指定的prompt后,从标准输入中读取并返回一行或多行。默认提示是一个>。如果multiline为 0,则读取一行。如果multiline不为 0,则读取多行。你需要按下Enter来停止输入文本。
ren(from, to) mv的同义词。
rm(filePath) 删除带有指定filePath的文件。
rmdir(dirPath) 用指定的dirPath删除目录。
which(cmd) 基于 path 环境变量打印指定的cmd命令的路径。
XMLDocument(input) 将可以是文件路径或Readerinput转换为 DOM 文档对象。如果没有指定input,则返回一个空的 DOM 文档。
XMLResult(input) 将任意流或文件转换为XMLResult。如果inputjavax.xml.transform.Result的实例,则返回input;如果inputorg.w3c.dom.Document的实例,则返回一个javax.xml.transform.dom.DOMResult;否则,返回一个javax.xml.transform.stream.StreamResult
XMLSource(input) 将任意流、文件、URL 转换为XMLSource。如果inputjavax.xml.transform.Source的实例,则返回input;如果inputorg.w3c.dom.Document的实例,则返回一个javax.xml.transform.dom.DOMSource;否则,返回一个javax.xml.transform.stream.StreamSource
XSLTransform(input, style, output) 执行 XSLT 转换;input是输入的 XMLstyle是 XML 样式表;output是输出 XML。Inputstyle可以是URLFileInputStream;输出可以是一个File或一个OutputStream

以下是使用jrunscript提供的一些实用函数的输出:

C:\>jrunscript

nashorn> cat("http://jdojo.com/about

68      : <p>You can contact Kishori Sharan by email at <a href="``mailto:ksharan@jdojo.com``">``ksharan@jdojo.com

nashorn> var addr = read("Please enter your address: ", 1);

Please enter your address: 9999 Main St.

Please enter your address: Dreamland, HH 11111

Please enter your address:

nashorn> print(addr)

9999 Main St.

Dreamland, HH 11111

nashorn> which("jrunscript.exe");

c:\JAVA8\BIN\jrunscript.exe

nashorn>pwd()

C:\

nashorn>

这些实用函数中的大部分都是利用 Java 类库作为 Nashorn 脚本编写的。了解这些函数如何工作的最好方法是阅读源代码。您可以通过在nashorn命令提示符下输入函数名来打印非本地函数的源代码。以下命令序列向您展示了如何打印exec(cmd)函数的源代码。输出显示该函数在内部使用 Java Runtime类来运行命令:

c:\>jrunscript

nashorn> exec

function exec(cmd) {

var process = java.lang.Runtime.getRuntime().exec(cmd);

var inp = new DataInputStream(process.getInputStream());

var line = null;

while ((line = inp.readLine()) != null) {

println(line);

}

process.waitFor();

$exit = process.exitValue();

}

nashorn> exit()

c:\>

还有另外三个由jrunscript提供的全局函数值得一提。这些函数可以用作函数和构造函数:

  • jlist(javaList)
  • jmap(javaMap)
  • JSInvoker(object)

jlist()函数接受java.util.List的一个实例,并返回一个 JavaScript 对象,您可以用它来访问List,就像它是一个数组一样。您可以使用带索引的括号符号来访问List的元素。返回的对象包含一个length属性,它给出了List的大小。清单 9-3 包含了显示如何使用jlist()函数的代码。

清单 9-3。使用 jlist()函数

// jlisttest.js

// Create an ArrayList and add two elements to it

var ArrayList = Java.type("java.util.ArrayList");

var list = new ArrayList();

list.add("Ken");

list.add("Li");

// Convert the ArrayList into a Nashorn array

var names = jlist(list);

print("Accessing an ArrayList as a Nashorn array...");

for(var i = 0; i < names.length; i++) {

printf("names[%d] = %s", i, names[i]);

}

下面的命令使用jrunscript命令行 shell 执行清单 9-3 中的代码:

C:\>jrunscript -f jlisttest.js

Accessing an ArrayList as a Nashorn array...

names[0] = Ken

names[1] = Li

jmap()函数接受java.util.Map的一个实例,并返回一个 JavaScript 对象,您可以用它来访问MapMap中的键成为 JavaScript 对象的属性。清单 9-4 包含了显示如何使用jmap()函数的代码。

清单 9-4。使用 jmap()函数

// jmaptest.js

// Create an HashMap and add two elements to it

var HashMap = Java.type("java.util.HashMap");

var map = new HashMap();

map.put("Ken", "(999) 777-3331");

map.put("Li", "(888) 444-1111");

// Convert the HashMap into a Nashorn object

var phoneDir = jmap(map);

print("Accessing a HashMap as a Nashorn object...");

for(var prop in phoneDir) {

printf("phoneDir['%s'] = %s", prop, phoneDir[prop]);

}

// Use dot notation to access the proeprty

var kenPhone = phoneDir.Ken; // Same as phoneDir["Ken"]

printf("phoneDir.Ken = %s", kenPhone)

下面的命令使用jrunscript命令行 shell 执行清单 9-4 中的代码:

C:\>jrunscript -f jmaptest.js

Accessing a HashMap as a Nashorn object...

phoneDir['Ken'] = (999) 777-3331

phoneDir['Li'] = (888) 444-1111

phoneDir.Ken = (999) 777-3331

JSInvoker()函数接受一个委托对象作为参数。当在JSInvoker对象上调用一个函数时,在委托对象上调用invoke(name, args)方法。被调用的函数名作为第一个参数传递给invoke()方法;传递给函数调用的参数作为第二个参数传递给invoke()方法。清单 9-5 显示了如何使用JSInvoker对象。

清单 9-5。使用 JSInvoker 对象

// jsinvokertest.js

var calcDelegate = { invoke: function(name, args) {

if (args.length !== 2) {

throw new Error("Must pass 2 arguments to " + name);

}

var value = 0;

if (name === "add")

value = args[0] + args[1];

else if (name === "subtract")

value = args[0] - args[1];

else if (name === "multiply")

value = args[0] * args[1];

else if (name === "divide")

value = args[0] / args[1];

else

throw new Error("Operation " + name + " not supported.");

return value;

}

};

var calc = new JSInvoker(calcDelegate);

var x = 20.44, y = 30.56;

var addResult = calc.add(x, y); // Will call calcDelegate.invoke("add", [x, y])

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

printf("calc.sub(%.2f, %.2f) = %.2f%n", x, y, subResult);

printf("calc.mul(%.2f, %.2f) = %.2f%n", x, y, mulResult);

printf("calc.div(%.2f, %.2f) = %.2f", x, y, divResult);

代码创建了一个名为calcDelegate的对象,它包含一个invoke()方法。JSInvoker对象包装了calcDelegate对象。当在calc对象上调用一个函数时,calcDelegate对象的invoke()方法被调用,函数名作为第一个参数,函数参数作为第二个参数以数组的形式传递。invoke()函数对参数执行加、减、乘、除操作。以下命令显示了如何执行代码:

c:\>jrunscript -f jsinvokertest.js

calc.add(20.44, 30.56) = 51.00

calc.sub(20.44, 30.56) = -10.12

calc.mul(20.44, 30.56) = 624.65

calc.div(20.44, 30.56) = 0.67

JSInvoker对象可以在 Java 7 中工作,但是当您运行这个例子时,会在 Java 8 中产生以下错误。好像是 Java 8 引入的 bug:

c:\>jrunscript -f jsinvokertest.js

script error in file jsinvoker.js : TypeError: [object JSAdapter] has no such function "add" in jsinvoker.js at line number 25

jrunscript shell 还为几个 Java 类创建了别名,比如java.io.File、j ava.io.Reader、j ava.net.URL等等,所以您可以通过它们的简单名称来引用它们。其他几个对象也被jrunscript曝光为全局对象。您可以使用以下命令在命令行上打印全局对象及其类型的完整列表。仅显示了部分输出。注意,输出还将包括一个名为p的属性,这是在for循环中声明的变量名。

c:\>jrunscript

nashorn> for(var p in this) print(p, typeof this[p]);

engine object

JSInvoker function

jmap function

jlist function

inStream function

outStream function

streamClose function

javaByteArray function

pwd function

...

nashorn>exit()

c:\

摘要

JDK 包括一个名为jrunscript的独立于脚本引擎的命令行 shell。它可用于评估在命令行或从文件中输入的脚本。您可以在JAVA_HOME\bin目录中找到这个 shell,其中JAVA_HOME是您安装 JDK 的目录。

jrunscript命令行 shell 可以运行用 Java 支持的任何脚本语言编写的脚本。默认情况下,它运行 Nashorn 脚本。要使用 Nashorn 之外的脚本语言,您需要使用–cp–classpath选项将该语言的 JAR 文件包含在jrunscript中。–l选项让您选择想要使用的脚本语言。

您可以在一行程序模式、批处理模式和交互模式下使用jrunscript。单行模式允许您执行一行脚本。使用–e选项调用一行程序模式。批处理模式允许您执行存储在文件中的脚本。使用–f选项调用批处理模式。交互模式允许您以交互方式执行在命令行上输入的脚本。不使用–e–f选项,或者使用–f –选项(注意–f后的)调用交互模式。

您可以使用–q选项用jrunscript列出所有可用的脚本引擎。注意,您必须包含除 Nashorn 之外的语言的脚本引擎的 JAR 文件,以使它们在jrunscript中可用。jrunscript shell 提供了几个有用的全局函数和对象。例如,cat()函数可以用来打印一个文件或 URL 的内容,可选地应用一个过滤器。

十、jjs命令行工具

在本章中,您将学习:

  • 什么是jjs命令行工具
  • 如何调用jjs命令行工具
  • 如何向jjs命令行工具传递参数
  • 如何在交互和脚本模式下使用jjs命令行工具

为了配合 Nashorn 脚本引擎,JDK 8 包含了一个名为jjs的新命令行工具。如果你想知道jjs代表什么,它代表 Java JavaScript。该命令位于JDK_HOME\bin目录下。该命令可用于运行文件中的脚本或以交互模式在命令行上输入的脚本。它还可以用于执行 shell 脚本。

语法

调用该命令的语法是:

jjs [options] [script-files] [-- arguments]

这里:

  • [options]jjs命令的选项。多个选项由空格分隔
  • [script-files]是由 Nashorn 引擎解释的脚本文件列表
  • [-- arguments]是作为参数传递给脚本或交互式 shell 的参数列表。参数在双连字符后指定,可以使用脚本中的arguments属性访问它们

选项

jjs工具支持几个选项。表 10-1 列出了jjs工具的所有选项。一些选项有一个以同样方式工作的变体;例如,选项-classpath-cp是同义词。两者都用于设置类路径。请注意,有些选项以两个连字符开头。要打印所有选项的列表,在命令提示符下运行带有–xhelp选项的工具:

jjs –xhelp

表 10-1。

Options for the jjs Comand-Line Tool

[计]选项 描述
-D<name>=<value> 为 Java 运行时设置系统属性。可以重复使用该选项来设置多个运行时属性值。
-ccs=<size> --class-cache-size=<size> 设置类缓存大小(以字节为单位)。默认情况下,类缓存大小设置为 50 字节。您可以在 size 中使用 k/K、m/M 和 g/G 来表示 KB、MB 和 GB 大小。选项-css=200-css=2M分别将类缓存大小设置为 200 字节和 2 MB。
-classpath <path> -cp <path> 指定类路径。
-co --compile-only 编译脚本而不运行。默认情况下,它是禁用的,这意味着脚本被编译和运行。
--const-as-var 用关键字var替换脚本中的关键字const。如果脚本使用了 Nashorn 无法识别的const关键词,则此选项可用。默认情况下,它是禁用的。
-d=<path> --dump-debug-dir=<path> 指定为脚本转储类文件的目标目录。
--debug-lines .class文件中生成行号表。默认情况下,它是启用的。指定--debug-lines=false禁用该功能。
--debug-locals .class文件中生成一个本地变量表。默认设置为false
-doe ––dump-on-error 如果指定了此选项,将打印错误的完整堆栈跟踪。默认情况下,会打印一条简短的错误消息。
--early-lvalue-error 解析代码时,将无效的lvalue表达式报告为早期错误。默认设置为true。如果设置为 false,则在执行代码时会报告无效的左值表达式。
--empty-statements 在 Java 抽象语法树(AST)中保留空语句。默认设置为false
-fv –fullversion 打印 Nashorn 引擎的完整版本。
--function-statement-error 当函数声明用作语句时,打印错误信息。默认设置为false
--function-statement-warning 当函数声明用作语句时,打印警告消息。默认设置为false
-fx 将脚本作为 JavaFX 应用启动。
--global-per-engine 每个脚本引擎实例使用一个全局实例。默认为false
-help -h 输出帮助消息并退出。
-J<flag> 将指定的<flag>传递给 JVM。
-language 指定 ECMAScript 语言版本。有效值为es5es6。默认是es5
--lazy-compilation 通过不一次性编译整个脚本来启用延迟代码生成策略。默认为true
--loader-per-compile 每次编译创建一个新的类装入器。默认为true
-l --locale 设置脚本执行的区域设置。默认为en-US
--log=subsystem:lebel 为给定数量的子系统启用给定级别的日志记录,例如--log=fields:finest,codegen:info。多个子系统的日志记录用逗号分隔。
-nj --no-java 禁用 Java 支持。缺省值是false,意味着允许在脚本中使用 Java 类。
-nse --no-syntax-extensions 不允许非标准语法扩展。默认为false
-nta --no-typed-arrays 禁用类型化数组支持。默认为false
--optimistic-types 使用乐观类型假设,并取消优化重新编译。默认为true
--parse-only 分析代码而不编译。默认为false
-pcc --persistent-code-cache 为编译的脚本启用磁盘缓存。默认为false
--print-ast 打印抽象语法树。默认为false
-pc --print-code 将生成的字节码打印到标准错误或指定的目录。您可以指定打印字节码的函数名。指定目录和功能的语法是:-pc=dir:<output-directory-path>,function:<function-name>
--print-lower-ast 打印降低的抽象语法树。默认为false
-plp --print-lower-parse 打印降低的解析树。默认为false
--print-mem-usage 打印每个编译阶段后指令寄存器(IR)的内存使用情况。默认为false
--print-no-newline print()函数在打印其参数后不会打印换行符。默认为false
-pp --print-parse 打印解析树。默认为false
--print-symbols 打印符号表。默认为false
-pcs --profile-callsites 转储调用点配置文件数据。默认为false
-scripting 启用外壳脚本功能。默认为false
--stderr=<filename&#124;stream&#124;tty> stderr重定向到指定的文件名、流或文本终端。
--stdout=<filename&#124;stream&#124;tty> stdout重定向到指定的文件名、流或文本终端。
-strict 启用使用 ECMAScript 版标准执行脚本的严格模式。默认为false
-t=<timezone> –timezone=<timezone> 设置脚本执行的时区。默认时区是芝加哥/美国。
-tcs=<option> --trace-callsites=<option> 启用调用点跟踪模式。有效选项有miss(跟踪调用点未命中)、enterexit(跟踪调用点进入/退出)和objects(打印对象属性)。指定多个选项,用逗号分隔:-tcs=miss,enterexit,objects
--verify-code 在运行之前验证字节码。默认为false
-v –version 打印 Nashorn 引擎的版本。默认为false
-xhelp 打印扩展帮助。默认为false

在交互模式下使用 jjs

如果在没有指定任何选项或脚本文件的情况下运行jjs,它将以交互模式运行。当您输入脚本时,它会被解释。回想一下,Nashorn 中的字符串可以用单引号或双引号括起来。

以下是在交互模式下使用jjs工具的一些例子。假设您已经在机器的 path 环境变量中包含了jjs工具的路径。如果您没有这样做,您可以在下面的命令中用JDK_HOME\bin\jjs替换jjs。记得执行quit()exit()功能退出jjs工具:

c:\>jjs

jjs> "Hello Nashorn"

Hello Nashorn

jjs> "Hello".toLowerCase();

hello

jjs> var list = [1, 2, 3, 4, 5]

jjs> var sum = 0;

jjs> for each (x in list) { sum = sum + x};

15

jjs> quit()

c:\>

向 jjs 传递参数

下面是一个向jjs工具传递参数的例子。前五个自然数作为参数传递给jjs工具,稍后使用arguments属性访问它们。请注意,您必须在两个连字符和第一个参数之间添加一个空格:

c:\>jjs -- 1 2 3 4 5

jjs> for each (x in``arguments

1

2

3

4

5

jjs> quit()

c:\>

考虑清单 10-1 中的脚本。该脚本已保存在名为stream.js的文件中。该脚本处理整数列表。该列表可以作为命令行参数传递给脚本。如果列表没有作为参数传递,它使用前五个自然数作为列表。它计算列表中奇数整数的平方和。它打印列表和总数。

清单 10-1。计算列表中奇数的平方和的脚本

// stream.js

var list;

if (arguments.length == 0) {

list = [1, 2, 3, 4, 5];

}

else {

list = arguments;

}

print("List of numbers: " + list);

var sumOfSquaredOdds = list.filter(function(n) {return n % 2 == 1;})

.map(function(n) {return n * n;})

.reduce(function(sum, n) {return sum + n;}, 0);

print("Sum of the squares of odd numbers: " + sumOfSquaredOdds);

使用jjs工具,您可以如下运行stream.js文件中的脚本。假设stream.js文件在当前目录中。否则,您需要指定文件的完整路径:

c:\>jjs stream.js

List of numbers: 1,2,3,4,5

Sum of the squares of odd numbers: 35

c:\>jjs stream.js -- 10 11 12 13 14 15

List of numbers: 10,11,12,13,14,15

Sum of the squares of odd numbers: 515

c:\>

在脚本模式下使用 jjs

可以在脚本模式下调用jjs工具,这允许您运行 shell 命令。您可以使用–scripting选项在脚本模式下启动jjs工具。shell 命令用反引号括起来,而不是单引号/双引号。以下是在脚本模式下使用jjs工具使用datels shell 命令的示例:

c:\>jjs -scripting

jjs> date``

Wed Oct 15 15:27:07 CDT 2014

jjs> ls -l``

total 3102

drwxr-xr-x  4 ksharan Administrators       0 Jan 11  2014 $AVG

drwxr-xr-x  5 ksharan Administrators       0 Jan 22  2014 $Recycle.Bin

-rw-r--r--  1 ksharan Administrators       1 Jun 18  2013 BOOTNXT

-rw-r--r--  1 ksharan Administrators      94 May 23  2013 DBAR_Ver.txt

More output goes here...

jjs> exit()

c:\>

Nashorn 在脚本模式下定义了几个全局对象和函数,如表 10-2 所列。

表 10-2。

Global Objects and Functions Available in Scripting Mode

全局对象 描述
$ARG 存储传递给脚本的参数。与arguments的工作方式相同。
$ENV 将所有环境变量映射到一个对象。
$EXEC(cmd, input) 用于在新进程中运行命令的全局函数,将input传递给cmd。两个参数都可以是命令,在这种情况下,input的输出将作为输入 Io cmd传递。
$OUT | 存储流程的最新标准输出。例如,执行$EXEC()的结果保存在$OUT中。
$ERR 存储流程的最新标准输出。
$EXIT 存储进程的退出代码。非零值表示进程失败。
echo(arg1, arg2,...) echo()函数的工作原理与print()函数相同,但它仅在脚本模式下可用。
readLine(prompt) 从标准输入中读取一行输入。指定的参数显示为提示。默认情况下,读取输入显示在标准输出上。该函数返回读取的输入。
readFully(filePath) 读取指定文件的全部内容。默认情况下,内容显示在标准输出中。您可以将函数的返回值赋给变量。

以下脚本显示了如何使用$ARG全局对象:

c:\>jjs -scripting -- 10 20 30

jjs> for each(var arg in $ARG) print(arg);

10

20

30

jjs>

以下脚本显示了如何使用$ENV全局对象。它在 Windows 上打印 OS 环境变量的值,并列出所有环境变量:

jjs> print($ENV.OS);

Windows_NT

jjs> for(var x in $ENV) print(x);

LOCALAPPDATA

PROCESSOR_LEVEL

FP_NO_HOST_CHECK

USERDOMAIN

LOGONSERVER

PROMPT

OS

...

以下脚本使用$EXEC()全局函数列出所有扩展名为txt的文件,其中包含ksharan:

jjs> $EXEC("grep -l ksharan *.txt");

test.txt

您可以在变量中捕获 shell 命令的输出。脚本模式允许在双引号括起来的字符串中进行表达式替换。请注意,表达式替换功能在单引号中的字符串中不可用。表达式被指定为${expression}。以下命令捕获变量中的date shell 命令的值,并使用表达式替换将日期值嵌入到字符串中。请注意,在示例中,当字符串用单引号括起来时,表达式替换不起作用:

c:\ >jjs -scripting

jjs> var today = date``

jjs> "Today is ${today}"

Today is Mon Jul 14 22:48:26 CDT 2014

jjs> 'Today is ${today}'

Today is ${today}

jjs> quit()

c:\>

您还可以使用脚本模式执行存储在文件中的 shell 脚本:

C:\> jjs –scripting myscript.js

jjs工具支持可以在脚本模式下运行的脚本文件中的 heredocs。heredoc 也称为 here 文档、here 字符串或 here 脚本。这是一个多行字符串,其中保留了空格。一个 heredoc 以一个双尖括号(< <)和一个定界标识符开始。通常,EOFEND被用作定界标识符。但是,您可以使用脚本中其他地方没有用作标识符的任何其他标识符。多行字符串从最后一行开始。字符串以相同的定界标识符结尾。以下是在 Nashorn 中使用 heredoc 的示例:

var str = <<EOF

This is a multi-line string using the heredoc syntax.

Bye Heredoc!

EOF

清单 10-2 包含了在 Nashorn 中使用 heredoc 的脚本。$ARG属性仅在脚本模式下定义,其值是使用jjs工具传递给脚本的参数:

清单 10-2。使用 heredoc 样式的 heredoc.js 文件的内容是一个多行字符串

// heredoc.js

var str = <<EOF

This is a multiline string.

Number of arguments passed to this

script is ${$ARG.length}

Arguments are ${$ARG}

Bye Heredoc!

EOF

print(str);

您可以执行如下所示的heredoc.js脚本文件:

c:\> jjs -scripting heredoc.js

This is a multi-line string.

Number of arguments passed to this

script is 0

Arguments are

Bye Heredoc!

c:\> jjs -scripting heredoc.js -- Kishori Sharan

This is a multi-line string.

Number of arguments passed to this

script is 2

Arguments are Kishori,Sharan

Bye Heredoc!

除了 Nashorn 支持的两种注释样式(///* */)之外,jjs工具还支持一种额外的以数字符号(#)开头的单行注释样式。如果jjs工具运行的脚本文件以符号#开头,jjs工具会自动启用脚本模式,并在脚本模式下执行整个脚本。考虑清单 10-3 所示的脚本。

清单 10-3。jjs 工具的特殊注释。内容存储在 jjscomments.js 文件中

# This script will run in scripting mode by the jjs tool

# because it starts with a number sign

// Set the current directory to C:\kishori

$ENV.PWD = "C:\\kishori";

// Get the list of files and directories in the current directory

var str = ls -F;

print(str);

以下命令运行jjscomments.js文件中的脚本。脚本以#符号开始,因此jjs工具将自动启用脚本模式:

c:\>jjs jjscomments.js

books/

ejb/

hello.txt

important/

programs/

rmi.log

rmi.policy

scripts/

c:\>

使用jjs工具将带有第一个#符号的脚本文件解释为 shell 可执行文件,您可以在脚本文件的开头使用 shebang ( #!)来将其作为脚本可执行文件运行。注意,类 Unix 操作系统直接支持 shebang。您需要在 shebang 中包含 jjs 工具的路径,这样脚本将由jjs工具执行。脚本文件将被传递给 jjs 工具来执行。因为脚本文件以#符号开始(?? 的一部分),jjs工具将自动启用脚本模式。以下是使用 shebang 的脚本示例,假设jjs工具位于/usr/bin目录:

#!/usr/bin/jjs

var str = ls -F;

print(str);

摘要

Java 8 提供了一个名为jjs的命令行工具。它位于JDK_HOME\bin目录中。它用于在命令行上运行 Nashorn 脚本。jjs工具支持许多选项。可以调用它在文件中、在交互模式下和在脚本模式下运行脚本。

如果不指定任何选项或脚本文件,jjs以交互模式运行。在交互模式下,脚本在输入时被解释。

如果您使用–scripting选项调用jjs,它将在脚本模式下运行,允许您使用任何特定于操作系统的 shell 命令。Shell 命令用反引号括起来。在脚本模式下,jjs工具支持脚本文件中的 heredocs。如果脚本文件以#符号开头,运行脚本文件会自动启用脚本模式。这支持执行包含 shebang ( #!在脚本的开头)的脚本。

十一、在 Nashorn 中使用 JavaFX

在本章中,您将学习:

  • jjs命令行工具中的 JavaFX 支持
  • 在脚本中提供 JavaFX Application类的init()start()stop()方法的实现
  • 如何使用预定义的脚本加载和使用 JavaFX 包和类
  • 使用 Nashorn 脚本创建和启动简单的 JavaFX 应用

在本章中,我们假设您已经有了 JavaFX 8 的初级经验。如果您没有 JavaFX 的经验,请在阅读本章之前学习 JavaFX。

jjs 中的 JavaFX 支持

jjs命令行工具允许您启动在 Nashorn 脚本中创建的 JavaFX 应用。您需要使用带有jjs工具的–fx选项来作为 JavaFX 应用运行脚本。以下命令将存储在myfxapp.js文件中的脚本作为 JavaFX 应用运行:

jjs –fx myfxapp.js

脚本中 JavaFX 应用的结构

在 JavaFX 应用中,您可以覆盖下面三个Application类的方法来管理应用的生命周期:

  • init()
  • start()
  • stop()

在 Nashorn 脚本中,您可以像在 Java 中一样管理 JavaFX 应用的生命周期。脚本中可以有三个名为init()start()stop()的函数。注意,在 Nashorn 脚本中,这三个函数都是可选的。这些函数对应于 Java 类中的三个方法,它们的调用顺序如下:

The init() function is called. You can initialize the application in this function.   The start() function is called. As is the case in a Java application, the start() function is passed the reference to the primary stage of the application. You need to populate the scene, add the scene to the primary stage, and show the stage.   The stop() function is called when the JavaFX application exits.   Tip

如果 JavaFX 应用的脚本中没有start()函数,那么全局范围内的整个脚本都被认为是start()函数的代码。除了这三个函数之外,还可以有其他函数。它们将被视为函数,没有任何特殊的含义。这些函数不会被自动调用;您需要在脚本中调用它们。

清单 11-1 包含了一个简单的 JavaFX 应用的 Java 代码。它显示一个窗口,在一个StackPane中有一个Text节点。HelloFX类包含了init()start()stop()方法。图 11-1 显示了HelloFX应用显示的窗口。当您退出应用时,stop()方法会在标准输出中显示一条消息。

清单 11-1。Java 中的 HelloFX 应用

// HelloFX.java

package com.jdojo.script;

import javafx.application.Application;

import javafx.scene.Scene;

import javafx.scene.layout.StackPane;

import javafx.scene.text.Font;

import javafx.scene.text.Text;

import javafx.stage.Stage;

public class HelloFX extends Application {

private Text msg;

private StackPane sp;

public static void main(String[] args) {

Application.launch(HelloFX.class);

}

@Override

public void init() {

msg = new Text("Hello JavaFX from Nashorn!");

msg.setFont(Font.font("Helvetica", 18));

sp = new StackPane(msg);

}

@Override

public void start(Stage stage) throws Exception {

stage.setTitle("Hello FX");

stage.setScene(new Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

@Override

public void stop() {

System.out.println("Hello FX application is stopped.");

}

}

Hello FX application is stopped.

A978-1-4842-0713-0_11_Fig1_HTML.jpg

图 11-1。

The Windows Displayed by the HelloFX Application

清单 11-2 包含了用于HelloFX应用的 Nashorn 脚本。它是清单 11-1 中 Java 代码的一对一翻译。请注意,用 Nashorn 编写的相同应用的代码要短得多。脚本存储在hellofx.js文件中。您可以使用如下命令提示符运行该脚本,它将显示如图 11-1 所示的相同窗口:

jjs –fx hellofx.js

清单 11-2。存储在 hellofx.js 中的 HelloFX 应用的 Nashorn 脚本

// hellofx.js

var msg;

var sp;

function init() {

msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

sp = new javafx.scene.layout.StackPane(msg);

}

function start(stage) {

stage.setTitle("Hello FX");

stage.setScene(new javafx.scene.Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

function stop() {

java.lang.System.out.println("Hello FX application is stopped.");

}

您不需要在脚本中拥有任何init()start()stop()函数。清单 11-3 包含了另一个版本的HelloFX应用。不包括init()stop()功能。来自init()函数的代码已经被移动到全局范围。stop()方法已经被移除,所以当应用退出时,您将不会在标准输出上看到消息。Nashorn 会先执行全局范围内的代码,然后调用start()方法。脚本存储在hellofx2.js文件中。运行它显示如图 11-1 所示的相同窗口。您可以运行该脚本:

jjs –fx hellofx2.js

清单 11-3。HelloFX 应用的另一个版本,没有 init()和 stop()方法

// hellofx2.js

var msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

var sp = new javafx.scene.layout.StackPane(msg);

function start(stage) {

stage.setTitle("Hello FX");

stage.setScene(new javafx.scene.Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

您可以进一步简化 HelloFX 应用的脚本。您可以从脚本中删除start()函数。JavaFX 运行时创建初级阶段的引用并将其传递给start()函数。如果没有start()功能,如何获取初级阶段的引用?Nashorn 创建了一个名为$STAGE的全局对象,它是对初级阶段的引用。您可以使用此全局对象来处理主要阶段。你甚至不需要展示初级阶段;Nashorn 会自动显示给你看。

清单 11-3 包含的脚本是同一个 HelloFX 应用的另一个版本。它使用全局对象$STAGE来引用初级阶段。我已经去掉了init()功能。这一次,你甚至没有调用初级阶段的show()方法。你在让 Nashorn 自动为你展示初级阶段。脚本保存在hellofx3.js文件中。您可以运行该脚本:

jjs –fx hellofx3.js

清单 11-4。HelloFX 应用的另一个版本,没有 init()、start()和 stop()函数

// hellofx3.js

var msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

var sp = new javafx.scene.layout.StackPane(msg);

$STAGE.setTitle("Hello FX");

$STAGE.setScene(new javafx.scene.Scene(sp, 300, 100));

$STAGE.sizeToScene();

// $STAGE.show(); // No need to show the primary stage. Nashorn will // automatically show it.

让我们再尝试一种功能组合。您将提供init()函数,但不提供start()函数。清单 11-5 包含了同一个 HelloFX 应用的代码。它包含了创建控件的init()方法,但是删除了start()方法。

清单 11-5。Nashorn 脚本中 JavaFX 应用的不正确实现

// incorrectfxapp.js

var msg;

var sp;

function init() {

msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

sp = new javafx.scene.layout.StackPane(msg);

}

$STAGE.setTitle("Hello FX");

$STAGE.setScene(new javafx.scene.Scene(sp, 300, 100));

$STAGE.sizeToScene();

当您运行清单 11-5 中的脚本时,它抛出一个异常,如下所示:

jjs –fx incorrectfxapp.js

Exception in Application start method

Exception in thread "main" java.lang.RuntimeException: Exception in Application start method

at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:875)

at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$149(LauncherImpl.java:157)

at com.sun.javafx.application.LauncherImpl$$Lambda$1/23211803.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

Caused by: java.lang.ClassCastException: Cannot cast jdk.nashorn.internal.runtime.Undefined to javafx.scene.Parent

at java.lang.invoke.MethodHandleImpl.newClassCastException(MethodHandleImpl.java:364)

...

当您试图使用全局变量sp创建场景时抛出异常,该变量是对StackPane的引用。与预期相反,在全局范围内运行代码之前,不会调用init()方法。在自动调用init()函数之前,先调用全局范围内的代码。在脚本中,init()方法创建了要添加到场景中的控件。当创建场景时,变量sp仍然是undefined,这导致了异常。如果您在脚本中显示了主要阶段,那么在主要阶段已经显示之后会调用init()函数。如果您让 Nashorn 为您显示初级阶段,则在初级阶段之前调用init()函数。

Tip

如果在 JavaFX 脚本中没有提供start()函数,那么提供init()函数几乎没有任何用处,因为这样的init()函数将在主阶段构建完成后被调用。如果您想使用init()函数来初始化您的 JavaFX 应用,您应该同时提供init()start()方法,以便它们被按顺序调用。

最后,我将向您展示最简单的 JavaFX 应用,它只用一行脚本就可以编写完成。它会在一个窗口中显示一条消息。但是,该窗口没有标题文本。清单 11-6 包含了一行脚本。它展示了 Nashorn 的妙处,将 10 到 15 行 Java 代码压缩成 1 行脚本!以下命令运行显示窗口的脚本,如图 11-2 所示:

jjs –fx simplestfxapp.js

清单 11-6。Nashorn 中最简单的 JavaFX 应用

// simplestfxapp.js

$STAGE.scene = new javafx.scene.Scene(new javafx.scene.control.Label("Hello JavaFX Scripting"));

A978-1-4842-0713-0_11_Fig2_HTML.jpg

图 11-2。

The simplest JavaFX application using Nashorn script

在 Nashorn 中,您只需为最简单的 JavaFX 应用编写一行代码,这是一种保守的说法。正确的说法是,您甚至不需要在 Nashorn 中编写一行代码来显示一个窗口。创建一个名为empty.js的脚本文件,不要在其中写任何代码。您可以将该文件命名为任何其他名称。使用以下命令运行empty.js文件:

jjs –fx empty.js

该命令将显示如图 11-3 所示的窗口。Nashorn 是如何在你不写任何代码的情况下显示一个窗口的?回想一下,Nashorn 创建了初级阶段和一个全局对象$STAGE来表示初级阶段。如果它看到你没有展示初级阶段,它就为你展示。这就是本案中发生的情况。脚本文件为空,Nashorn 自动显示空的初级阶段。

A978-1-4842-0713-0_11_Fig3_HTML.jpg

图 11-3。

The simplest JavaFX application using a Nashorn script without writing even one line of code

导入 JavaFX 类型

您可以使用 JavaFX 类的完全限定名,或者使用Java.type()函数导入它们。在上一节中,您使用了所有 JavaFX 类的完全限定名。下面的代码片段展示了在 JavaFX 中创建Label的两种方法:

// Using the``fully qualified name

var msg = new javafx.scene.control.Label("Hello JavaFX!");

// Using Java.type() function

var Label = Java.type("javafx.scene.control.Label");

var msg = new Label("Hello JavaFX!");

键入所有 JavaFX 类的完全限定名可能很麻烦。脚本不是应该比 Java 代码短吗?Nashorn 有一种方法可以缩短 JavaFX 脚本。它包括几个脚本文件,这些文件将 JavaFX 类型作为它们的简单名称导入。在脚本中使用 JavaFX 类的简单名称之前,您需要使用load()方法加载这些脚本文件。例如,Nashorn 包含一个fx:controls.js脚本文件,它将所有 JavaFX 控件类作为它们的简单类名导入。表 11-1 包含脚本文件和它们导入的类/包的列表。

表 11-1。

The List of Nashorn Script Files and the Classes/Packages That They Import

Nashorn 脚本文件 导入的类/包
fx:base.js javafx.stage.Stage javafx.scene.Scene javafx.scene.Group javafx/beans javafx/collections
fx:graphics.js javafx/animation javafx/application javafx/concurrent javafx/css javafx/geometry javafx/print javafx/scene javafx/stage
fx:controls.js javafx/scene/chartjavafx/scene/control
fx:fxml.js javafx/fxml
fx:web.js javafx/scene/web
fx:media.js javafx/scene/media
fx:swing.js javafx/embed/swing
fx:swt.js javafx/embed/swt

下面的代码片段显示了如何加载这个脚本文件并使用简单的名称javafx.scene.control.Label类:

// Import all JavaFX control class names

load("fx:controls.js")

// Use the simple name of the Label control

var msg = new Label("Hello JavaFX!");

清单 11-7 包含 JavaFX 欢迎应用的代码,它保存在一个名为greeter.js的文件中。您可以按如下方式运行脚本:

jjs –fx greeter.js

清单 11-7。使用 Nashorn 脚本的 JavaFX 应用

// greeter.js

// Load Nashorn predefined scripts to import JavaFX specific classes // and packages

load("fx:base.js");

load("fx:controls.js");

load("fx:graphics.js");

// Define the start() method of the JavaFX application class

function start(stage) {

var nameLbl = new Label("Enter your name:");

var nameFld = new TextField();

var msg = new Label();

msg.style = "-fx-text-fill: blue;";

// Create buttons

var sayHelloBtn = new Button("Say Hello");

var exitBtn = new Button("Exit");

// Add the event handler for the Say Hello button

sayHelloBtn.onAction = sayHello;

// Call the same fucntion sayHello() when the user

nameFld.onAction = sayHello;

// Add the event handler for the Exit button

exitBtn.onAction = function() {

Platform.exit();

};

// Create the root node

var root = new VBox();

root.style = "-fx-padding: 10;" +

"-fx-border-style: solid inside;" +

"-fx-border-width: 2;" +

"-fx-border-insets: 5;" +

"-fx-border-radius: 5;" +

"-fx-border-color: blue;";

// Set the vertical spacing between children to 5px

root.spacing = 5;

// Add children to the root node

root.children.addAll(msg, new HBox(nameLbl, nameFld, sayHelloBtn, exitBtn));

// Set the scene and title for the stage

stage.scene = new Scene(root);

stage.title = "Greeter";

// Show the stage

stage.show();

// A nested function to say hello based on the entered name

function sayHello(evt) {

var name = nameFld.getText();

if (name.trim().length() > 0) {

msg.text = "Hello " + name;

}

else {

msg.text = "Hello there";

}

}

}

欢迎应用显示一个窗口,如图 11-4 所示。输入名称并按下Enter键或点击Say Hello按钮。将显示带有问候语的讯息。

A978-1-4842-0713-0_11_Fig4_HTML.jpg

图 11-4。

The greeter JavaFX aplication in action

在 Nashorn 中开发 JavaFX 应用要容易得多。在脚本中,您可以使用属性调用 Java 对象的 getters 和 setters。可以直接访问所有 Java 对象的属性,而不仅仅是 JavaFX 对象的属性。比如不用 Java 写root.setSpacing(5),可以用 Nashorn 写root.spacing = 5

为按钮添加事件处理程序也更容易。您可以设置一个匿名函数作为按钮的事件处理程序。请注意,您可以使用onAction属性来设置事件处理程序,而不是调用Button类的setOnAction()方法。下面的代码片段展示了如何使用函数引用sayHello来设置按钮的ActionEvent处理程序:

// Add the event handler for the Say Hello button

sayHelloBtn.onAction = sayHello

注意,在这个例子中,您在start()函数中使用了一个嵌套函数sayHello()。对函数的引用被用作事件处理程序。一个事件处理器接受一个参数,sayHello()函数中的evt形参就是那个事件对象。

摘要

Nashorn 中的jjs命令行工具支持启动用脚本编写的 JavaFX 应用。–fx选项与jjs一起使用来启动 JavaFX 应用。JavaFX 应用的脚本可以有与 JavaFX Application类的init()start()stop()方法相对应的init()start()stop()函数。Nashorn 调用这些函数的顺序与在 JavaFX 应用中调用它们的顺序相同。

start()函数传递对初级阶段的引用。如果你不提供一个start()函数,整个脚本被认为是start()函数。Nashorn 提供了一个名为$STAGE的全局对象,它是对初级阶段的引用。如果你没有提供start()函数,你需要使用$STAGE全局变量来访问初级阶段。如果您没有提供start()方法,也没有显示初级阶段,Nashorn 将为您调用$STAGE.show()方法。

十二、Nashorn 的 Java APIs

在本章中,您将学习:

  • Nashorn 的 Java APIs 是什么
  • 如何直接实例化 Nashorn 引擎
  • 如何在 Java 代码中和命令行上将选项传递给 Nashorn 引擎
  • 如何在 Nashorn 引擎中的脚本上下文之间共享全局
  • 如何在 Java 代码中添加、更新、删除和读取脚本对象的属性
  • 如何在 Java 代码中创建脚本对象并调用它们的方法
  • 如何从 Java 代码中调用脚本函数
  • 如何将脚本日期转换成 Java 日期

Nashorn 的 Java APIs 是什么?

在 Nashorn 脚本中使用 Java 类很简单。有时您可能想在 Java 代码中使用 Nashorn 对象。您可以将 Nashorn 对象传递给 Java 代码,或者 Java 代码可以评估 Nashorn 脚本并检索 Nashorn 对象的引用。当 Nashorn 对象跨越边界(脚本到 Java)时,它们需要表示为 Java 类的对象,并且您应该能够像使用任何其他 Java 对象一样使用它们。

如果您的应用只使用 Nashorn,为了充分利用 Nashorn 引擎,您可能希望使用 Nashorn 中可用的选项和扩展。您需要用 Java 代码实例化 Nashorn 引擎,使用特定于 Nashorn 引擎的类,而不是使用 Java 脚本 API 中的类。

Nashorn 的 Java APIs 提供了 Java 类和接口,允许您直接在 Java 代码中处理 Nashorn 脚本引擎和 Nashorn 对象。图 12-1 描述了当你处理 Nashorn 引擎时,你应该在客户端代码中使用的那些类和接口的类图。它们在jdk.nashorn.api.scripting包里。

A978-1-4842-0713-0_12_Fig1_HTML.jpg

图 12-1。

A class diagram of Java APIs for Nashorn in the jdk.nashorn.api.scripting package

注意,Nashorn 脚本引擎在内部使用了其他包中的许多其他类。然而,你不应该在你的应用中直接使用它们,除了来自jdk.nashorn.api.scripting包的类。位于wiki . open JDK . Java . net/display/Nashorn/Nashorn+JSR 223+engine+notes的网页包含了jdk.nashorn.api.scripting包文档的链接。

Note

JDK8u40 中已经增加了jdk.nashorn.api.scripting包中的ClassFilter接口,计划 2015 年一季度末出货。如果您想更早地使用这个接口,您需要下载 JDK8u40 的早期访问版本。

我将在后续章节中详细讨论一些特定于 Nashorn 的 Java 类和接口。表 12-1 列出了jdk.nashorn.api.scripting包中的类和接口及其描述。

表 12-1。

The List of Java Classes/Interfaces to be Used with Nashorn Scripting Engine

类别/接口 描述
NashornScriptEngineFactory 这是 Nashorn 引擎的脚本引擎工厂实现。当您想要创建一个使用 Nashorn 特定选项和扩展的 Nashorn 引擎时,您需要实例化这个类。
NashornScriptEngine 这是 Nashorn 引擎的脚本引擎实现类。不要直接实例化这个类。它的实例是使用一个NashornScriptEngineFactory对象的getScriptEngine()方法获得的。
NashornException 这是从 Nashorn 脚本抛出的所有异常的基本异常类。当您使用 Java 脚本 API 时,您的 Java 代码接收到一个ScriptException类的实例,该实例将成为NashornException的包装器。如果您直接从 Java 代码中访问脚本,例如通过在脚本中实现 Java 接口并在 Java 代码中使用该接口的实例,则可能会在 Java 代码中直接抛出NashornException的实例。该类包含许多方法,这些方法使您可以访问脚本错误的详细信息,如行号、列号、脚本的错误对象等。
ClassFilter ClassFilter是一个接口。您可以使用它的实例来限制 Nashorn 脚本中部分或全部 Java 类的可用性。当使用一个NashornScriptEngineFactory实例化一个NashornScriptEngine时,您将需要传递这个接口的一个实例。
ScriptUtils 一个用于 Nashorn 脚本的实用程序类。
URLReader 读取 URL 的内容。它继承自java.io.Reader类。
JSObject 这个接口的一个实例用 Java 代码表示一个 Nashorn 对象。如果您想将一个 Java 对象传递给一个应该被视为 Nashorn 对象的 Nashorn 脚本,您需要传递这个接口的一个实例。您可以在 Nashorn 脚本中使用括号符号来访问和设置此类 Java 对象的属性。
AbstractJSObject 这是一个实现了JSObject接口的抽象类。
ScriptObjectMirror 这是一个镜像对象,它包装了一个 Nashorn 脚本对象。它继承自AbstractJSObject类并实现了Bindings接口。当 Java 代码从脚本中接收到一个 Nashorn 对象时,脚本对象在被传递给 Java 代码之前被包装在一个ScriptObjectMirror实例中。

实例化 Nashorn 引擎

在前面的章节中,您已经使用标准的 Java 脚本 API 用 Java 代码实例化了 Nashorn 引擎。Nashorn 引擎提供了几个自定义功能。为了利用这些特性,您需要直接实例化 Nashorn 引擎。首先,您需要创建一个NashornScriptEngineFactory类的对象,然后调用getScriptEngine()方法的一个重载版本来创建一个 Nashorn 引擎:

// Create a Nashorn engine factory

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();

// Create a Nashorn engine with default options

ScriptEngine engine = factory.getScriptEngine();

默认情况下,Nashorn 引擎工厂创建一个启用了--dump-on-error选项的 Nashorn 引擎。在下一个示例中,我将向您展示如何为 Nashorn 引擎设置其他选项。

以下代码片段使用--no-java–strict选项创建了一个 Nashorn 引擎:

// Create a Nashorn engine factory

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();

// Store the Nashorn options in a String array

String[] options = {"--no-java", "-strict"};

// Create the Nashorn engine with the options

ScriptEngine engine = factory.getScriptEngine(options);

由于使用了--no-java选项,您不能在这个引擎执行的脚本中使用任何 Java 类。–strict选项将强制引擎以严格模式执行所有脚本。

您还可以在命令行上使用nashorn.args系统属性将选项传递给 Nashorn 引擎。以下命令运行com.jdojo.script.Test类,将四个选项传递给 Nashorn 引擎:

java -Dnashorn.args="--global-per-engine -strict --no-java --language=es5" com.jdojo.script.Test

请注意,选项由空格分隔,它们都作为一个字符串传递。如果只有一个要传递的选项,可以省略选项值两边的双引号。以下命令只将一个选项--no-java传递给引擎:

java -Dnashorn.args=--no-java com.jdojo.script.Test

下面的代码片段使用类过滤器创建了一个 Nashorn 引擎,它是ClassFilter接口的一个实例,用来限制来自com.jdojo包及其子包的任何类的使用。注意 JDK8u40 中增加了ClassFilter接口;它在 JDK8 中不可用:

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();

ScriptEngine engine = factory.getScriptEngine(clsName -> clsName.startsWith("com.jdojo"));

ClassFilter是功能界面。它的方法被传递了 Nashorn 脚本试图使用的 Java 类的完全限定名。如果方法返回true,则可以在脚本中使用该类;否则,该类不能在脚本中使用。

Tip

如果不希望脚本中暴露任何 Java 类,可以使用clsName -> false作为ClassFilter的 lambda 表达式。在脚本中使用受ClassFilter限制的 Java 类会抛出java.lang.ClassNotFound异常。

下面是NashornScriptEngineFactory类的getScriptEngine()方法的重载版本列表:

  • ScriptEngine getScriptEngine()
  • ScriptEngine getScriptEngine(String... args)
  • ScriptEngine getScriptEngine(ClassFilter classFilter)
  • ScriptEngine getScriptEngine(ClassLoader appLoader)
  • ScriptEngine getScriptEngine(String[] args, ClassLoader appLoader)
  • ScriptEngine getScriptEngine(String[] args, ClassLoader appLoader, ClassFilter classFilter)

共享引擎全局

默认情况下,Nashorn 引擎维护每个脚本上下文的全局对象。在本讨论中,术语“全局”指的是存储在 Nashorn 脚本的全局作用域中的全局变量和声明,您在顶级脚本中通过this引用它们。不要将脚本上下文中的全局范围绑定与脚本中的全局范围绑定混淆。当您在脚本中引用任何变量或创建变量时,会首先搜索脚本全局变量。如果您在脚本中引用了一个在脚本全局中找不到的变量,引擎将在脚本上下文的全局范围bindings中搜索它。例如,脚本对象ObjectMathString等等都是脚本全局的一部分。考虑清单 12-1 中的程序及其输出。

清单 12-1。每个 Nashron 引擎使用多个脚本全局变量

// MultiGlobals.java

package com.jdojo.script;

import javax.script.ScriptContext;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.SimpleScriptContext;

public class MultiGlobals {

public static void main(String[] args) {

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Add a variable named msg to the script globals

engine.eval("var msg = 'Hello globals'");

// Print the value of the msg variable

engine.eval("print(this.msg);");

// Execute the same script as above, but using a new

// ScriptContext object. The engine will use a fresh

// copy of the globals and will not find this.msg that

// was created and associated with the default script

// context of the engine previously.

ScriptContext ctx = new SimpleScriptContext();

engine.eval("print(this.msg);", ctx);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

Hello globals

undefined

该程序执行以下步骤:

  • 使用默认选项创建 Nashorn 引擎。
  • 使用使用引擎默认脚本上下文的eval()方法执行脚本。该脚本创建了一个名为msg的全局变量,它存储在脚本 globals 中。你可以在全局范围内使用简单的名字msgthis.msg来引用变量。
  • 使用eval()方法执行脚本,该方法使用打印msg变量的值的引擎的默认脚本上下文。输出中的第一行确认打印出了msg变量的正确值。
  • 使用使用新脚本上下文的eval()方法执行脚本。该脚本试图打印全局变量msg的值。输出中的第二行,通过打印undefined,确认名为msg的变量在脚本 globals 中不存在。

这是 Nashorn 引擎的默认行为。它为每个脚本上下文创建全局变量的新副本。如果您想要使用默认上下文的全局变量(如果您想要共享全局变量),您可以通过将默认上下文的引擎作用域Bindings复制到您的新脚本上下文来实现。清单 12-2 使用这种方法在两个脚本上下文之间共享脚本全局变量。

清单 12-2。通过复制引擎默认上下文的引擎范围绑定来共享脚本全局变量

// CopyingGlobals.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.ScriptContext;

import static javax.script.ScriptContext.ENGINE_SCOPE;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.SimpleScriptContext;

public class CopyingGlobals {

public static void main(String[] args) {

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Add a variable named msg to the global scope of // the script

engine.eval("var msg = 'Hello globals'");

// Print the value of the msg value

engine.eval("print(this.msg);");

// Create a ScriptContext and copy the ENGINE_SCOPE // Bindings of the default

// script context to the new ScriptContext

ScriptContext ctx = new SimpleScriptContext();

ScriptContext defaultCtx = engine.getContext();

Bindings engineBindings = defaultCtx.getBindings(ENGINE_SCOPE);

ctx.setBindings(engineBindings, ENGINE_SCOPE);

// Use the new ScriptContext to execute the script

engine.eval("print(this.msg);", ctx);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

Hello globals

Hello globals

Nashorn 引擎的–-global-per-engine选项完成了与前一个例子相同的事情。它在所有脚本上下文中共享脚本全局变量。清单 12-3 显示了如何为引擎设置这个选项。输出确认引擎仅使用一个全局副本来执行所有脚本。

清单 12-3。在 Nashorn 引擎中的所有脚本上下文之间共享脚本全局

// SharedGlobals.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.ScriptContext;

import static javax.script.ScriptContext.ENGINE_SCOPE;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.SimpleScriptContext;

import jdk.nashorn.api.scripting.NashornScriptEngineFactory;

public class SharedGlobals {

public static void main(String[] args) {

// Get the Nashorn script engine using the // --global-per_engine option

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();

ScriptEngine engine = factory.getScriptEngine("--global-per-engine");

try {

// Add a variable named msg to the global scope of // the script

engine.eval("var msg = 'Hello globals'");

// Print the value of the msg value

engine.eval("print(this.msg);");

// Execute the same script, but using a new

// ScriptContext. Note that the script globals // are shared and this script will find the

// this.msg variable created by the first // script execution.

ScriptContext ctx = new SimpleScriptContext();

engine.eval("print(this.msg);", ctx);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

Hello globals

Hello globals

在 Java 代码中使用脚本对象

当来自 Nashorn 的对象和值跨越 script-Java 边界进入 Java 代码时,它们需要被表示为 Java 对象和值。表 12-2 列出了脚本对象和它们对应的 Java 对象之间的类映射。

表 12-2。

The List of Mapping Between Classes of Script Objects and Java Objects

脚本对象的类型 Java 对象的类型
不明确的 jdk.nashorn.internal.runtime.Undefined
null
数字 java.lang.Number
布尔代数学体系的 java.lang.Boolean
原始字符串类型 java.lang.String
任何脚本对象 jdk.nashorn.api.scripting.ScriptObjectMirror
任何 Java 对象 The same as the Java object in the script

注意,您不应该使用jdk.nashorn.internal包及其子包中的类和接口。表中列出它们只是为了提供信息。在此表中,Nashorn 数字类型的映射显示为java.lang.Number类型。尽管 Nashorn 试图尽可能地为数值传递java.lang.Integer和 j ava.lang.Double对象,但是依赖这样的专用 Java 类型是不可靠的。当一个 Nashorn 数从脚本传递到 Java 代码时,您应该会看到一个java.lang.Number实例。只要有必要,Nashorn 中的 Number 和 Boolean 类型的值都会被转换成相应的 Java 原语类型,比如intdoubleboolean等等。

在 Nashorn 中,脚本对象被表示为jdk.nashorn.internal.runtime.ScriptObject类的一个实例。当脚本对象被传递给 Java 代码时,它被包装在一个ScriptObjectMirror对象中。在 JDK8u40 之前,如果你将一个脚本对象传递给一个声明其参数为java.lang.Object类型的 Java 方法,传递的是一个ScriptObject,而不是一个ScriptObjectMirror。JDK8u40 改变了这种行为,每次都将一个脚本对象作为ScriptObjectMirror传递给 Java 代码。如果您在 Java 中将方法的参数声明为JSObjectScriptObjectMirror,Nashorn 总是将脚本对象作为ScriptObjectMirror传递给 Java。

Tip

脚本可以将值undefined传递给 Java 代码。您可以使用ScriptObjectMirror类的isUndefined(Object obj)静态方法来检查传递给 Java 的脚本对象是否是undefined。如果指定的objundefined,则该方法返回 true 否则,它返回 false。

ScriptObjectMirror类实现了JSObjectBindings接口。该类添加了更多的方法来处理脚本对象。您可以将 Java 代码中脚本对象的引用存储在三种类型的变量中的任何一种:JSObjectBindingsScriptObjectMirror。你用什么类型取决于你的需要。使用ScriptObjectMirror类型给你更多的灵活性和访问脚本对象的所有特性。表 12-3 包含了在JSObject接口中声明的方法列表。

我将在接下来的小节中展示如何使用这些方法和ScriptObjectMirror类的一些方法。任何 Java 类都可以实现JSObject接口。这种类的对象可以像脚本对象一样使用,并且可以使用语法obj.func()obj[prop]delete obj.prop等在脚本中处理它们的方法和属性。

表 12-3。

The List of Methods Declared in the JSObject Interface with Their Descriptions

方法 描述
Object call(Object thiz, Object... args) 将此对象作为函数调用。当JSObject包含 Nashorn 函数或方法的引用时,您将使用该方法。参数thiz被用作函数调用中this的值。args中的参数作为参数传递给被调用的函数。这个方法在 Java 中的工作方式与func.apply(thiz, args)在 Nashorn 脚本中的工作方式相同。
Object eval(String script) 评估指定的脚本。
String getClassName() 返回对象的 ECMAScript 类名。这和 Java 里的类名不一样。
Object getMember(String name) 返回脚本对象的命名属性的值。
Object getSlot(int index) 返回脚本对象的索引属性的值。
boolean hasMember(String name) 如果脚本对象具有命名属性,则返回 true 否则返回 false。
boolean hasSlot(int index) 如果脚本对象具有索引属性,则返回 true 否则返回 false。
boolean isArray() 如果脚本对象是数组对象,则返回 true 否则返回 false。
boolean isFunction() 如果脚本对象是函数对象,则返回 true 否则返回 false。
boolean isInstance(Object instance) 如果指定的instance是该对象的实例,则返回 true 否则返回 false。
boolean isInstanceOf(Object clazz) 如果该对象是指定的clazz的实例,则返回 true 否则返回 false。
boolean isStrictFunction() 如果该对象是严格函数,则返回 true 否则返回 false。
Set<String> keySet() 以一组字符串的形式返回该对象的所有属性的名称。
Object newObject(Object... args) 调用此方法的对象应该是构造函数对象。调用此构造函数创建一个新对象。这相当于 Nashorn 脚本中的new func(arg1, arg2...)
void removeMember(String name) 从对象中移除指定的属性。
void setMember(String name, Object value) 将指定的value设置为该对象的指定属性名。如果属性名不存在,则添加一个具有指定name的新属性。
void setSlot(int index, Object value) 将指定的value设置为该对象的指定索引属性。如果index不存在,则添加一个具有指定index的新属性。
double toNumber() 返回对象的数值。通常,如果脚本对象是数值的包装器,比如在 Nashorn 中使用表达式new Object(234.90)创建的脚本对象,您将使用此方法。在其他脚本对象上,它返回值Double.NAN
Collection<Object> values() 在一个Collection中返回该对象的所有属性值。

使用脚本对象的属性

ScriptObjectMirror类实现了JSObjectBindings接口。您可以使用这两个接口的方法来访问属性。您可以使用JSObjectgetMember()getSlot()方法来读取脚本对象的命名和索引属性。您可以使用Bindingsget()方法来获取属性的值。您可以使用JSObjectsetMember()setSlot()方法来添加和更新属性。Bindingsput()方法可以让你做同样的事情。

您可以使用JSObjecthasMember()hasSlot()方法来检查命名属性和索引属性是否存在。同时,您可以使用Bindings接口的containsKey()方法来检查一个属性是否存在。您可以认为由ScriptObjectMirror类实现的JSObjectBindings接口提供了同一个脚本对象的两个视图——前者是一个简单的对象,后者是一个地图。

清单 12-4 包含一个 Nashorn 脚本,它创建两个脚本对象并调用一个 Java 类的两个方法,如清单 12-5 所示。该脚本将脚本对象传递给 Java 方法。Java 方法打印传递的对象的属性,并向两个脚本对象添加一个新属性。该脚本在方法返回后打印属性,以确认 Java 代码中添加到脚本对象的新属性仍然存在。

清单 12-4。一个 Nashorn 脚本,它调用 Java 方法并将脚本对象传递给它们

// scriptobjectprop.js

// Create an object

var point = {x: 10, y: 20};

// Create an array

var empIds = [101, 102];

// Get the Java type

var ScriptObjectProperties = Java.type("com.jdojo.script.ScriptObjectProp");

// Pass the object to Java

ScriptObjectProperties.propTest(point);

// Print all properties of the point object

print("In script, after calling the Java method propTest()...");

for(var prop in point) {

var value = point[prop];

print(prop + " = " + value);

}

// Pass the array object to Java

ScriptObjectProperties.arrayTest(empIds);

// Print all elements of teh empIds array

print("In script, after calling the Java method arrayTest()...");

for(var i = 0, len = empIds.length; i < len; i++) {

var value = empIds[i];

print("empIds[" + i + "] = " + value);

}

清单 12-5。一个 Java 类,其方法访问脚本对象的属性

// ScriptObjectProp.java

package com.jdojo.script;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

import jdk.nashorn.api.scripting.ScriptObjectMirror;

public class ScriptObjectProp {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "scriptobjectprop.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full // path of the script file and terminate the program.

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath.toAbsolutePath()

+ " does not exist.");

return;

}

// Create a scripting engine manager

ScriptEngineManager manager = new ScriptEngineManager();

// Obtain a Nashorn scripting engine from the manager

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Execute the script that will call the propTest() and

// arrayTest() methods of this class

engine.eval("load('" + scriptFileName + "')");

}

catch (ScriptException e) {

e.printStackTrace();

}

}

public static void propTest(ScriptObjectMirror point) {

// List all properties

System.out.println("Properties of point received in Java...");

for (String prop : point.keySet()) {

Object value = point.getMember(prop);

System.out.println(prop + " = " + value);

}

// Let us add a property named z

System.out.println("Adding z = 30 to point in Java... ");

point.setMember("z", 30);

}

public static void arrayTest(ScriptObjectMirror empIds) {

if (!empIds.isArray()) {

System.out.println("Passed in obejct is not an array.");

return;

}

// Get the length proeprty of teh array

int length = ((Number) empIds.getMember("length")).intValue();

System.out.println("empIds received in Java...");

for (int i = 0; i < length; i++) {

int value = ((Number) empIds.getSlot(i)).intValue();

System.out.printf("empIds[%d] = %d%n", i, value);

}

// Let us add an element to the array

System.out.println("Adding empIds[2] = 103 in Java... ");

empIds.setSlot(length, 103);

}

}

Properties of point received in Java...

x = 10

y = 20

Adding z = 30 to point in Java...

In script, after calling the Java method propTest()...

x = 10

y = 20

z = 30

empIds received in Java...

empIds[0] = 101

empIds[1] = 102

Adding empIds[2] = 103 in Java...

In script, after calling the Java method arrayTest()...

empIds[0] = 101

empIds[1] = 102

empIds[2] = 103

在 Java 中创建 Nashorn 对象

您可能在脚本中有构造函数,并希望使用这些构造函数在 Java 中创建对象。ScriptObjectMirror类的newObject()方法允许您在 Java 中创建脚本对象。方法声明是:

Object newObject(Object... args)

方法的参数是要传递给构造函数的参数。方法返回新的对象引用。此方法需要在构造函数上调用。首先,您需要获取 Java 中构造函数的引用作为一个ScriptObjectMirror对象,然后调用这个方法使用这个构造函数创建一个新的脚本对象。

您可以使用ScriptObjectMirror类的callMember()方法调用脚本对象的方法。该方法的声明是:

Object callMember(String methodName, Object... args)

在第四章中,你创建了一个名为Point的构造函数。它的声明如清单 12-6 所示,供您参考。在这个例子中,您将在 Java 中创建Point类型的对象。

清单 12-6。Nashorn 中名为 Point 的构造函数的声明

// Point.js

// Define the Point constructor

function Point(x, y) {

this.x = x;

this.y = y;

}

// Override the toString() method in Object.prototype

Point.prototype.toString = function() {

return "Point(" + this.x + ", " + this.y + ")";

};

// Define a new method called distance()

Point.prototype.distance = function(otherPoint) {

var dx = this.x - otherPoint.x;

var dy = this.y - otherPoint.y;

var dist = Math.sqrt(dx * dx + dy * dy);

return dist;

};

清单 12-7 包含了执行下列步骤的 Java 程序:

  • 加载将加载Point构造函数定义的point.js脚本文件
  • 通过调用engine.get("Point")获取Point构造函数的引用
  • 通过调用Point构造函数上的newObject()方法创建两个名为p1p2Point对象
  • 调用p1上的callMember()方法来调用它的distance()方法来计算p1p2之间的距离
  • 调用p1p2上的callMember()方法,通过调用在Point构造函数中声明的toString()方法来获取它们的字符串表示
  • 最后,打印两点之间的距离

清单 12-7。用 Java 代码创建 Nashorn 对象

// CreateScriptObject.java

package com.jdojo.script;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import jdk.nashorn.api.scripting.ScriptObjectMirror;

public class CreateScriptObject {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "point.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full

// path of the script file and terminate the program.

if (! Files.exists(scriptPath) ) {

System.out.println(scriptPath.toAbsolutePath() +

" does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Execute the script in the file

engine.eval("load('" + scriptFileName + "');");

// Get the Point constructor as a ScriptObjectMirror // object

ScriptObjectMirror pointFunc = (ScriptObjectMirror)engine.get("Point");

// Create two Point objects. The following statements // are the same as var p1 = new Point(10, 20);

// and var p2 = new Point(13, 24); in a Nashorn script

ScriptObjectMirror p1 = (ScriptObjectMirror)pointFunc.newObject(10, 20);

ScriptObjectMirror p2 = (ScriptObjectMirror)pointFunc.newObject(13, 24);

// Compute the distance between p1 and p2 calling

// the distance() method of the Point object

Object result = p1.callMember("distance", p2);

double dist = ((Number)result).doubleValue();

// Get the string forms of p1 and p2 by calling // their toString() method

String p1Str = (String)p1.callMember("toString");

String p2Str = (String)p2.callMember("toString");

System.out.printf("The distance between %s and %s is %.2f.%n",

p1Str, p2Str, dist);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

The distance between Point(10, 20) and Point(13, 24) is 5.00.

从 Java 调用脚本函数

您可以从 Java 代码中调用脚本函数。可以从脚本向 Java 代码传递脚本函数对象,或者它可以通过脚本引擎获得函数对象的引用。您可以在作为函数对象引用的ScriptObjectMirror上使用call()方法。该方法的声明是:

Object call(Object thiz, Object... args)

第一个参数是在函数调用中用作this的对象引用。第二个参数是传递给函数的参数列表。要在全局范围内调用函数,使用null作为第一个参数。

在第四章中,你创建了一个factorial()函数来计算一个数的阶乘。这个函数的声明如清单 12-8 所示。

清单 12-8。factorial()函数的声明

// factorial.js

// Returns true if n is an integer. Otherwise, returns false.

function isInteger(n) {

return typeof n === "number" && isFinite(n) && n%1 === 0;

}

// Define a function that computes and returns the factorial of an integer

function factorial(n) {

if (!isInteger(n)) {

throw new TypeError("The number must be an integer. Found:" + n);

}

if(n < 0) {

throw new RangeError("The number must be greater than 0\. Found: " + n);

}

var fact = 1;

for(var counter = n; counter > 1; fact *= counter--);

return fact;

}

清单 12-9 包含的 Java 程序从factorial.js文件加载脚本,在ScriptObjectMirror中获取factorial()函数的引用,使用call()方法调用factorial()函数,并打印结果。最后,程序使用ScriptObjectMirror类的call()方法调用String对象上的Array.prototype.join()函数。

清单 12-9。从 Java 代码调用脚本函数

// InvokeScriptFunctionInJava.java

package com.jdojo.script;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import jdk.nashorn.api.scripting.ScriptObjectMirror;

public class InvokeScriptFunctionInJava {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "factorial.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full

// path of the script file and terminate the program.

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath.toAbsolutePath()

+ " does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Execute the script in the file, so teh factorial() function is loaded

engine.eval("load('" + scriptFileName + "');");

// Get the reference of the factorial() script function

ScriptObjectMirror factorialFunc =                                  (ScriptObjectMirror)engine.get("factorial");

// Invoke the factorial function and print the result

Object result = factorialFunc.call(null, 5);

double factorial = ((Number) result).doubleValue();

System.out.println("Factorial of 5 is " + factorial);

/* Call the Array.prototype.join() function on a String object */

// Get the reference of the Array.prototype.join method

ScriptObjectMirror arrayObject =

(ScriptObjectMirror)engine.eval("Array.prototype");

ScriptObjectMirror joinFunc =

(ScriptObjectMirror)arrayObject.getMember("join");

// Call the join() function of Array.prototype on a // string object passing a hyphen as a separator

String thisObject = "Hello";

String separator = "-";

String joinResult = (String)joinFunc.call(thisObject, separator);

System.out.println(joinResult);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

Factorial of 5 is 120.0

H-e-l-l-o

ScriptObjectMirror类包含其他几个方法来处理 Java 代码中的脚本对象。您可以使用seal()freeze()preventExtensions()方法来密封、冻结和阻止脚本对象的扩展。它们的工作原理与 Nashorn 脚本中的Object.seal()Object.freeze()Object.preventExtensions()方法相同。该类包含一个名为to()的实用方法,声明如下:

<T> T to(Class<T> type)

您可以使用to()方法将脚本对象转换为指定的type

将脚本日期转换为 Java 日期

你将如何在 Java 中创建一个日期对象,比如从一个脚本Date对象创建一个java.time.LocalDate对象?将脚本日期转换成 Java 日期并不简单。这两种日期有一个共同点:它们使用相同的纪元,即 UTC 1970 年 1 月 1 日午夜。您需要获取 JavaScript 日期中自纪元以来经过的毫秒数,创建一个Instant,并从Instant创建一个ZonedDateTime。清单 12-10 包含了一个完整的程序来演示 JavaScript 和 Java 之间的日期转换。运行这个程序时,您可能会得到不同的输出。在输出中,JavaScript 日期的字符串形式不包含毫秒部分,而 Java 对应部分包含毫秒部分。然而,这两个日期在内部表示同一时刻。

清单 12-10。将脚本日期转换为 Java 日期

// ScriptDateToJavaDate.java

package com.jdojo.script;

import java.time.Instant;

import java.time.ZoneId;

import java.time.ZonedDateTime;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import jdk.nashorn.api.scripting.ScriptObjectMirror;

public class ScriptDateToJavaDate {

public static void main(String[] args) {

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Create a Date object in script

ScriptObjectMirror jsDt = (ScriptObjectMirror)engine.eval("new Date()");

// Get the string representation of the script date

String jsDtString = (String) jsDt.callMember("toString");

System.out.println("JavaScript Date: " + jsDtString);

// Get the epoch milliseconds from the script date

long jsMillis = ((Number) jsDt.callMember("getTime")).longValue();

// Convert the milliseconds from JavaScript date to // java Instant

Instant instant = Instant.ofEpochMilli(jsMillis);

// Get a ZonedDateTime for the system default zone id

ZonedDateTime zdt =

ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());

System.out.println("Java ZonedDateTime: " + zdt);

}

catch (Exception e) {

e.printStackTrace();

}

}

}

JavaScript Date: Fri Oct 31 2014 18:17:02 GMT-0500 (CDT)

Java ZonedDateTime: 2014-10-31T18:17:02.489-05:00[America/Chicago]

摘要

Nashorn 的 Java APIs 提供了 Java 类和接口,允许您直接在 Java 代码中处理 Nashorn 脚本引擎和 Nashorn 对象。Nashorn 的所有 Java APIs 类都在jdk.nashorn.api.scripting包中。注意,Nashorn 脚本引擎在内部使用了其他包中的许多其他类。然而,您不应该在您的应用中直接使用它们,除了来自jdk.nashorn.api.scripting包的类。

Nashorn 脚本可以使用不同类型的值和对象,例如数字、字符串、原始值的布尔类型以及对象、字符串、日期和自定义对象。您也可以在脚本中创建 Java 对象。Number 和 Boolean 类型的脚本值在 Java 中表示为java.lang.Numberjava.lang.Boolean。脚本中的空类型在 Java 中表示为null。脚本中的 String 原语类型在 Java 中表示为java.lang.String。在 Java 中,脚本对象被表示为ScriptObjectMirror类的对象。

jdk.nashorn.api.scripting包中的NashornScriptEngineFactoryScriptEngine类分别代表 Nashorn 脚本引擎工厂和脚本引擎。如果您想将选项传递给 Nashorn 引擎,您将需要直接使用这些类。您还可以在命令行上使用-Dnashorn.args="<options>"将选项传递给 Nashorn 引擎。如果您将--globals-per-engine传递给 Nashorn 引擎,所有脚本上下文将共享全局变量。默认情况下,脚本上下文不共享全局变量。

ScriptObjectMirror类包含几个从脚本对象添加、更新和删除属性的方法。该类实现了JSObjectBindings接口。JSObject接口让您将脚本对象视为简单的 Java 类,而Bindings接口让您将脚本对象视为地图。您可以使用getMember()getSlot()get()方法来获取脚本对象的属性值。您可以使用setMember()setSlot()put()方法来添加或更新属性值。您可以使用callMember()方法来调用脚本对象的方法。call()方法允许您调用脚本函数,允许您为函数调用传递this值。ScriptObjectMirror类包含其他几个方法,让您在 Java 程序中使用脚本对象。

十三、调试、跟踪和分析脚本

在本章中,您将学习:

  • 如何在 NetBeans IDE 中调试独立的 Nashorn 脚本
  • 如何在 NetBeans IDE 中调试从 Java 代码调用的 Nashorn 脚本
  • 如何跟踪和分析 Nashorn 脚本

带有 JDK 8 或更高版本的 NetBeans 8 支持调试、跟踪和分析 Nashorn 脚本。您可以在 NetBeans IDE 中运行和调试独立的 Nashorn 脚本。当从 Java 代码调用 Nashorn 脚本时,您也可以调试它们。您可以使用调试脚本的所有调试功能来调试 Java 代码;您可以设置断点、显示变量值、添加观察器、监视调用堆栈等等。调试 Nashorn 脚本时,调试器显示 Nashorn 堆栈。

在 NetBeans 中,所有与调试器相关的窗格都可以使用菜单项Windows ➤ Debugging打开。有关 NetBeans 中完整调试功能的列表,请参考 NetBeans 中的帮助页面。当 NetBeans 应用处于活动状态时,您可以通过按下F1键来打开帮助页面。在本章中,我将使用清单 13-1 中的脚本作为调试脚本的例子。

清单 13-1。包含 isPrime()函数和对该函数的调用的测试脚本

// primetest.js

function isPrime(n) {

// Integers <= 2, floating-point numbers, and even numbers are not         // primes

if (n <= 2 || Math.floor(n) !== n || n % 2 === 0) {

return false;

}

// Check if n is divisible by any odd integers between 3 and sqrt(n).

var sqrt = Math.sqrt(n);

for (var i = 3; i <= sqrt; i += 2) {

if (n % i === 0) {

return false;

}

}

return true; // If we get here, it is a prime number.

}

// Check few nubmers for being primes

var num = 8;

var isPrimeNum = isPrime(num);

print(num + " is a prime number: " + isPrimeNum);

debugger;

num = 37;

isPrimeNum = isPrime(num);

print(num + " is a prime number: " + isPrimeNum);

调试独立脚本

要在 NetBeans 中运行或调试独立的 Nashorn 脚本,首先需要在 NetBeans IDE 中打开脚本文件。图 13-1 显示了在 NetBeans 中打开的清单 13-1 所示的脚本。要运行脚本,在显示脚本的编辑器中右击并选择Run File菜单项。或者,在脚本窗格激活时按下Shift + F6

A978-1-4842-0713-0_13_Fig1_HTML.jpg

图 13-1。

A Nashorn script opened in the NetBeans IDE

要调试脚本,您需要使用以下三种方法之一添加断点:

  • 将光标放在要设置/取消设置断点的行上。右键选择菜单项Toggle Line Breakpoint,设置和取消设置断点。
  • 将光标放在您想要设置/取消设置断点的行上,然后按Ctrl + F8。第一次按下此组合键会设置断点。如果已经设置了断点,则按下相同的组合键会取消设置断点。
  • 在脚本中添加debugger语句。当调试会话处于活动状态时,debugger语句就像一个断点。否则没有效果。

图 13-2 显示了在第 21 行和第 26 行有两个断点的相同脚本。

A978-1-4842-0713-0_13_Fig2_HTML.jpg

图 13-2。

A Nashorn script opened in the NetBeans IDE that has two breakpoints

现在您已经准备好调试脚本了。在脚本窗格中右击并选择Debug File菜单项。或者,当脚本窗格激活时,按下Ctrl + Shift + F5。它将启动调试会话,如图 13-3 所示。调试器在第一个断点处停止,如图所示。在底部,您会看到Variables窗格打开,显示所有变量及其值。如果您想查看调试会话的其他细节,请使用主菜单Window ➤ Debugging下的一个菜单项。如果您关闭任何调试窗格,如Variables窗格,您可以使用Window ➤ Debugging菜单重新打开它们。

A978-1-4842-0713-0_13_Fig3_HTML.jpg

图 13-3。

A Nashorn script opened in the NetBeans IDE in an active debugging session

当调试器会话处于活动状态时,您可以使用调试操作,如单步执行、单步跳过、单步退出、继续等。这些操作可从名为Debug的主菜单项以及调试器工具栏中获得。图 13-4 显示了调试器工具栏。默认情况下,当调试器会话处于活动状态时,它会出现。如果调试器会话激活时不可见,右击工具栏区域并选择Debug菜单项使其可见。表 13-1 包含脚本可用的调试操作及其快捷方式和描述。

表 13-1。

The List of Debugger Actions in NetBeans

调试操作 捷径 描述
完成调试器 Shift + F5 结束调试会话。
中止 停止当前会话中的所有线程。
继续 Ctrl + F5 继续执行程序,直到下一个断点。
跨过 F8 执行当前行并将程序计数器移动到文件中的下一行。如果执行的行是对函数的调用,那么函数中的代码也会被执行。
跳过表达式 Shift + F8 使您能够继续执行表达式中的每个方法调用,并查看每个方法调用的输入参数和结果输出值。如果没有进一步的方法调用,它的行为类似于单步执行操作。
进入 F7 执行当前行。如果该行是对一个函数的调用,并且被调用的函数有可用的源代码,则程序计数器移到该函数的声明处。否则,程序计数器移动到脚本中的下一行。
走出去 Ctrl + F7 执行当前函数中的其余代码,并将程序计数器移动到函数调用方之后的行。如果您单步执行了不再需要调试的函数,请使用此操作。

A978-1-4842-0713-0_13_Fig4_HTML.jpg

图 13-4。

The items in the debugger toolbar in NetBeans IDE

从 Java 代码调试脚本

从 Java 代码调用的调试脚本的工作方式略有不同。仅仅在脚本文件中设置断点并启动调试器或从 Java 代码进入脚本是不起作用的。您将使用清单 13-2 所示的 Java 程序来调试清单 13-1 所示的脚本。Java 程序使用一个Reader来执行来自primetest.js文件的脚本。但是,您也可以使用load()功能。在清单 13-2 中,您可以用下面的代码片段替换try-catch块中的代码,程序将会同样工作;您需要删除两个从java.io包中导入类的 import 语句:

try {

// Execute the script in the file

engine.eval("load('" + scriptFileName + "');"); // First time, add a                                              // breakpoint here

}

catch (ScriptException e) {

e.printStackTrace();

}

清单 13-2。调试从 Java 代码调用的脚本

// PrimeTest.java

package com.jdojo.script;

import java.io.IOException;

import java.io.Reader;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class PrimeTest {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "primetest.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full

// path of the script file and terminate the program.

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath.toAbsolutePath() +"                  "does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Get a Reader for the script file

Reader scriptReader = Files.newBufferedReader(scriptPath);

// Execute the script in the file

engine.eval(scriptReader);  // First time, add a   // breakpoint here

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

}

}

作为调试脚本的第一步,您需要在调用脚本引擎的eval()方法的代码行设置一个断点。如果不执行此步骤,您将无法从调试器单步执行脚本。图 13-5 显示了在第 35 行有一个断点的PrimeTest类的代码。

A978-1-4842-0713-0_13_Fig5_HTML.jpg

图 13-5。

The code of the PrimeTest class with a breakpint at line 35 in the NetBeans IDE

下一步是启动调试器。当包含PrimeTest类的编辑器窗格处于活动状态时,您可以使用Ctrl + Shift + F5。调试器将在第 35 行的断点处停止。你需要通过按F7进入eval()方法调用;这将把你带到AbstractScriptEngine.java文件,如图 13-6 所示。

A978-1-4842-0713-0_13_Fig6_HTML.jpg

图 13-6。

The debugger window when the AbstractScriptEngine.java file is being debugged

按下F7进入eval()方法调用。调试器将打开一个名为<eval>.js的文件,其中包含您试图使用Reader通过 Java 代码加载的primetest.js文件中的脚本。您可以滚动脚本并在<eval>.js文件中设置断点。图 13-7 显示了带有两个断点的文件——一个在第 21 行,一个在第 27 行。

A978-1-4842-0713-0_13_Fig7_HTML.jpg

图 13-7。

The debugger window showing the loaded script in the .js file

<eval>.js文件的脚本中设置了断点之后,就可以继续正常的调试操作了。例如,Continue调试动作(F5)会在下一个断点停止执行。

当调试器结束时,您可以从 Java 代码中移除断点,在本例中,Java 代码是PrimaTest.java文件。如果您启动一个新的调试会话,调试器将在您之前在<eval>.js文件中设置的断点处停止。请注意,您只需进入<eval>.js文件一次。所有后续调试会话都会记住先前会话中的断点。

Tip

即使 Nashorn 支持debugger语句,NetBeans IDE 似乎也不会将其识别为 Nashorn 脚本中的断点。当调试器处于活动状态时,在脚本中添加debugger语句不会暂停执行。

跟踪和分析脚本

Nashorn 支持调用点跟踪和分析。您可以在jjs命令行工具以及嵌入式 Nashorn 引擎中启用这些选项。您可以为引擎运行的所有脚本或每个脚本/函数启用跟踪和分析。–tcs选项为所有脚本启用调用点跟踪,并在标准输出中打印调用点跟踪信息。-pcs选项为所有脚本启用调用点分析,并在当前目录下名为NashornProfile.txt的文件中打印调用点分析数据。

您可以在脚本或函数的开头使用以下四个 Nashorn 指令,有选择地跟踪和分析整个脚本或函数:

  • "nashorn callsite trace enterexit"; // Equivalent to -tcs=enterexit
  • "nashorn callsite trace miss";      // Equivalent to -tcs=miss
  • "nashorn callsite trace objects";   // Equivalent to -tcs=objects
  • "nashorn callsite profile";         // Equivalent to -pcs

Tip

–tcs–pcs选项基于每个脚本引擎工作,而四个跟踪和分析指令基于每个脚本和每个函数工作。

这些 Nashorn 指令仅在调试模式下启用。您可以通过将nashorn.debug系统属性设置为 true 来启用 Nashorn 调试模式。这些指令在 JDK8u40 和更高版本中可用。在撰写本书时,JDK8u40 仍在开发中。清单 13-3 显示了一个为函数启用了 Nashorn callsite 配置文件选项的脚本。该脚本已保存在名为primeprofiler.js的文件中。

清单 13-3。为函数启用了 Nashorn Callsite 配置文件指令的脚本

// primeprofiler.js

function isPrime(n) {

// Profile this function only

"nashorn callsite profile";

// Integers <= 2, floating-point numbers, and even numbers are not primes

if (n <= 2 || Math.floor(n) !== n || n % 2 === 0) {

return false;

}

// Check if n is divisible by any odd integers between 3 and sqrt(n).

var sqrt = Math.sqrt(n);

for (var i = 3; i <= sqrt; i += 2) {

if (n % i === 0) {

return false;

}

}

return true; // If we get here, it is a prime number.

}

// Check few nubmers for being primes

var num = 8;

var isPrimeNum = isPrime(num);

print(num + " is a prime number: " + isPrimeNum);

num = 37;

isPrimeNum = isPrime(num);

print(num + " is a prime number: " + isPrimeNum);

以下命令在启用 Nashorn 调试选项的情况下运行primeprofile.js文件中的脚本:

c:\>jjs -J-Dnashorn.debug=true primeprofile.js

8 is a prime number: false

37 is a prime number: true

C:\

该命令将在当前直接生成一个名为NashornProfile.txt的文件,该文件包含isPrime()函数调用的概要数据。这个文件的内容如清单 13-4 所示。

清单 13-4。NashornProfile.txt 文件的内容

0        dyn:getProp|getElem|getMethod:Math        438462        2

1        dyn:getMethod|getProp|getElem:floor       433936        2

2        dyn:call                                  650602        2

3        dyn:getProp|getElem|getMethod:Math        313834        1

4        dyn:getMethod|getProp|getElem:sqrt        283356        1

5        dyn:call                                       0        1

清单 13-5 包含设置nashorn.debug系统属性并运行清单 13-3 所示脚本的 Java 程序。运行该程序将在当前目录下创建一个NashornProfile.txt文件,该文件的内容与清单 13-2 所示的相同。

清单 13-5。设置 nashorn.debug 系统属性和分析脚本

// ProfilerTest.java

package com.jdojo.script;

import java.io.IOException;

import java.io.Reader;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ProfilerTest {

public static void main(String[] args) {

// Set the nashorn.debug system property, so the tracing and

// profiling directives will be recognized

System.setProperty("nashorn.debug", "true");

// Construct the script file path

String scriptFileName = "primeprofiler.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full

// path of the script file and terminate the program.

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath.toAbsolutePath() +                    "does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JavaScript");

try {

// Get a Reader for the script file

Reader scriptReader = Files.newBufferedReader(scriptPath);

// Execute the script in the file

engine.eval(scriptReader);

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

}

}

8 is a prime number: false

37 is a prime number: true

摘要

带有 JDK 8 或更高版本的 NetBeans 8 支持在 NetBeans IDE 中调试 Nashorn 脚本。您可以在 NetBeans IDE 中运行和调试独立的 Nashorn 脚本。当从 Java 代码调用 Nashorn 脚本时,您也可以调试它们。调试器无缝地从 Java 代码跳到 Nashorn 脚本。

Nashorn 支持调用点跟踪和分析。您可以在jjs命令行工具以及嵌入式 Nashorn 引擎中启用这些选项。您可以为引擎运行的所有脚本或每个脚本/函数启用跟踪和分析。–tcs选项为引擎运行的所有脚本启用调用点跟踪。-pcs选项为引擎运行的所有脚本启用调用点分析,并在当前目录中名为NashornProfile.txt的文件中打印调用点分析数据。JDK8u40 增加了四个 Nashorn 指令:"nashorn callsite trace enterexit""nashorn callsite trace miss""nashorn callsite trace objects""nashorn callsite profile"。这些指令可以添加到脚本和函数的开头,以便有选择地跟踪和分析脚本和函数。它们仅在调试模式下工作,可通过将系统属性nashorn.debug设置为 true 来启用。

posted @ 2024-08-06 16:38  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报