1. 简介
本文聚焦于Nashorn——Java 8起成为JVM默认的JavaScript引擎。相比其前身Rhino,Nashorn采用了多种先进技术,性能提升了数个数量级,这次升级非常值得。
下面我们探索几种常见的使用方式。
2. 命令行
JDK 1.8内置了命令行解释器jjs,可用于运行JavaScript文件,或直接启动为REPL(交互式shell):
$ $JAVA_HOME/bin/jjs hello.js
Hello World
其中hello.js文件仅包含一行指令:print("Hello World");
同样代码可通过交互方式运行:
$ $JAVA_HOME/bin/jjs
jjs> print("Hello World")
Hello World
在*nix系统中,可在脚本首行添加#!$JAVA_HOME/bin/jjs
声明解释器:
#!$JAVA_HOME/bin/jjs
var greeting = "Hello World";
print(greeting);
然后直接执行脚本:
$ ./hello.js
Hello World
3. 嵌入式脚本引擎
更常见的用法是通过ScriptEngine在JVM中运行JavaScript。JSR-223定义了脚本API规范,提供可插拔的脚本引擎架构(当然,需要目标语言有JVM实现)。
创建JavaScript引擎示例:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
Object result = engine.eval(
"var greeting='hello world';" +
"print(greeting);" +
"greeting");
这里先创建ScriptEngineManager,然后获取名为nashorn的引擎。执行指令后返回结果——字符串"hello world"。
4. 向脚本传递数据
通过定义Bindings对象并作为eval的第二个参数传递数据:
Bindings bindings = engine.createBindings();
bindings.put("count", 3);
bindings.put("name", "baeldung");
String script = "var greeting='Hello ';" +
"for(var i=count;i>0;i--) { " +
"greeting+=name + ' '" +
"}" +
"greeting";
Object bindingsResult = engine.eval(script, bindings);
执行结果为:"Hello baeldung baeldung baeldung"。
5. 调用JavaScript函数
从Java代码调用JavaScript函数非常直接:
engine.eval("function composeGreeting(name) {" +
"return 'Hello ' + name" +
"}");
Invocable invocable = (Invocable) engine;
Object funcResult = invocable.invokeFunction("composeGreeting", "baeldung");
返回结果:"Hello baeldung"。
6. 使用Java对象
既然运行在JVM上,JavaScript代码中可以直接使用原生Java对象。通过Java对象实现:
Object map = engine.eval("var HashMap = Java.type('java.util.HashMap');" +
"var map = new HashMap();" +
"map.put('hello', 'world');" +
"map");
7. 语言扩展
Nashorn以ECMAScript 5.1为目标,但提供了一些扩展让JavaScript更易用。
7.1 使用For-Each遍历集合
For-each是简化集合遍历的便捷扩展:
String script = "var list = [1, 2, 3, 4, 5];" +
"var result = '';" +
"for each (var i in list) {" +
"result+=i+'-';" +
"};" +
"print(result);";
engine.eval(script);
输出结果:*1-2-3-4-5-*。
7.2 函数字面量
简单函数声明可省略花括号:
function increment(in) ++in
⚠️ 仅适用于单行函数。
7.3 条件Catch子句
可添加带条件的catch子句,仅当条件满足时执行:
try {
throw "BOOM";
} catch(e if typeof e === 'string') {
print("String thrown: " + e);
} catch(e) {
print("this shouldn't happen!");
}
输出:"String thrown: BOOM"。
7.4 类型数组与类型转换
支持Java类型数组,并与JavaScript数组互转:
function arrays(arr) {
var javaIntArray = Java.to(arr, "int[]");
print(javaIntArray[0]);
print(javaIntArray[1]);
print(javaIntArray[2]);
}
Nashorn会自动执行类型转换。调用arrays([100, "1654", true])
输出:100, 1654, 1(字符串和布尔值被隐式转换为整数)。
7.5 使用Object.setPrototypeOf设置原型
Nashorn提供API扩展用于修改对象原型:
Object.setPrototypeOf(obj, newProto)
✅ 这是比Object.prototype.__proto__
更推荐的写法。
7.6 魔法方法__noSuchProperty__和__noSuchMethod__
可定义特殊方法,在访问不存在属性或调用不存在方法时触发:
var demo = {
__noSuchProperty__: function (propName) {
print("Accessed non-existing property: " + propName);
},
__noSuchMethod__: function (methodName) {
print("Invoked non-existing method: " + methodName);
}
};
demo.doesNotExist;
demo.callNonExistingMethod()
输出:
Accessed non-existing property: doesNotExist
Invoked non-existing method: callNonExistingMethod
7.7 使用Object.bindProperties绑定属性
Object.bindProperties可将一个对象的属性绑定到另一个对象:
var first = {
name: "Whiskey",
age: 5
};
var second = {
volume: 100
};
Object.bindProperties(first, second);
print(first.volume);
second.volume = 1000;
print(first.volume);
注意这是"实时"绑定,源对象修改会立即反映到目标对象。
7.8 位置信息
通过全局变量获取当前文件名、目录和行号:__FILE__
, __DIR__
, __LINE__
:
print(__FILE__, __LINE__, __DIR__)
7.9 String.prototype扩展
Nashorn在String原型上添加了两个实用方法:trimRight和trimLeft:
print(" hello world".trimLeft());
print("hello world ".trimRight());
两次输出均为"hello world"(无首尾空格)。
7.10 Java.asJSONCompatible函数
此函数生成与Java JSON库兼容的对象。当对象(或其可访问对象)是JavaScript数组时,会实现List接口:
Object obj = engine.eval("Java.asJSONCompatible(
{ number: 42, greet: 'hello', primes: [2,3,5,7,11,13] })");
Map<String, Object> map = (Map<String, Object>)obj;
System.out.println(map.get("greet"));
System.out.println(map.get("primes"));
System.out.println(List.class.isAssignableFrom(map.get("primes").getClass()));
输出:"hello"、[2, 3, 5, 7, 11, 13]、true。
8. 加载脚本
可在ScriptEngine中加载其他JavaScript文件:
load('classpath:script.js')
也可从URL加载:
load('/script.js')
⚠️ JavaScript无命名空间概念,所有内容都加载到全局作用域,容易产生命名冲突。使用loadWithNewGlobal可避免此问题:
var math = loadWithNewGlobal('classpath:math_module.js')
math.increment(5);
对应的math_module.js:
var math = {
increment: function(num) {
return ++num;
}
};
math;
通过这种方式甚至可实现基础模块化!
9. 结论
本文探索了Nashorn JavaScript引擎的核心特性。实际项目中,建议将脚本保存在独立文件中,通过Reader类加载(而非直接使用字符串字面量)。
所有示例代码可在GitHub仓库获取。