1. 简介

本文聚焦于Nashorn——Java 8起成为JVM默认的JavaScript引擎。相比其前身RhinoNashorn采用了多种先进技术,性能提升了数个数量级,这次升级非常值得。

下面我们探索几种常见的使用方式。

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原型上添加了两个实用方法:trimRighttrimLeft

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仓库获取。


原始标题:Introduction to Nashorn

« 上一篇: Project Reactor Bus介绍
» 下一篇: 快速了解PMD