JavaScript 引擎:Rhino

Rhino 是 Mozilla 以 Java 寫出來的 JavaScript 引擎,長年維護下來,功能穩定。

Rhino 只需 java.exe 就能啟動,不需用到 javac.exe,因此 JRE + Rhino 的組合可以取代笨重的 JDK。不想用 Java 語言寫程式、也不想用 Kotlin,不妨試試 Rhino!

雖然透過 Rhino 跑 Java 程式,執行性能會明顯下降,但能用 JavaScript 語言快敲程式碼,短時間內敲出一堆 Java 功能來跑,那爽度不是蓋的。


準備


下載

http://github.com/mozilla/rhino

Java SE 6 和 Java SE 7 有內建 Rhino,但只打包部分功能,所以還是建議下載新版的來用。

Rhino 從 1.7.8 版開始,使用 Java SE 8 編譯,所以 Java SE 6/7 能用的最新版只到 1.7.7.2。

Java SE 8 起,改內建 Nashorn,兩者語法不相容,所以要從 Rhino 轉換 Nashorn 的話,不妨考慮下載 Rhino 來掛載就好,要比修改程式碼省事。


直譯 *.js 檔

java -jar rhino.jar *.js
使用 -jar 參數的話,CLASSPATH 只能存取 rhino.jar 裡面的檔案,無法存取 JAR 外部的檔案,所以最好改用 -cp 參數:

java -cp .;rhino.jar org.mozilla.javascript.tools.shell.Main *.js
此外,若 *.js 原始碼檔案使用不含 BOM 的 UTF-8 字元編碼,在 Windows 會有亂碼的問題。這時要下 -encoding UTF-8 參數給 org.mozilla.javascript.tools.shell.Main 或 rhino.jar,也可以下 -Dfile.encoding=UTF-8 參數給 java.exe。

其他常用的參數有:

-e 檔案執行 *.js 檔案。
-f 檔案讀取純文字文件,並嘗試執行裡面的內容,類似 eval(String)。
-opt -1 到 9設定編譯的最佳化程度,0 表示關閉最佳化,編譯速度最快,程式品質最差。依次從 1 到 9 提升最佳化程度。還可設定為 -1 表示直譯就好,不用編譯。
-strict全域範疇預設使用 ECMAScript 5 的嚴格模式,但函式範疇還是要下 "strict mode"。
-version 版號指定 SpiderMonkey 版號,有 100、110、120、130、140、150、160、170、180、200 可設定。
-help查閱更多參數。

編譯 *.js 為 *.class

java -cp rhino.jar org.mozilla.javascript.tools.jsc.Main script.js
編譯出來的 *.class 是 Rhino 的 NativeFunction 物件,所以只用 java.exe 無法執行,得掛載 Rhino:

java -cp .;rhino.jar script


Interactive mode

java -jar rhino.jar
Rhino 1.7.13 2020 09 02
js> _


目錄


基本:使用 Rhino.jar 直譯 JavaScript 程式檔

在 JavaScript 使用 Java API
調用第三方套件
在 JavaScript 使用 Java 的資料型態
調用 Java API 傳入 Java 的類別時
覆載物件方法與實作介面功能
多執行緒
在 *.js 中載入其它 *.js
最頂層物件
列出 JavaScript 物件的屬性和方法
Rhino 內建屬性和函式
其它 Rhino 的不標準語法


進階:使用 JDK 6/7 內建的 Rhino 引擎(非必要,可略過)

在 Java 寫 JavaScript 程式
使用新版 Rhino 在 Java 寫 JavaScript 程式
從 Java 傳資料給 JavaScript 程式
在 Java 載入 *.js 檔案
直接用 JDK 執行 *.js 檔
在 JDK 6/7 將 *.js 編譯成 *.class
進入 JDK 內建的 Interactive mode
如何取得 Java 內建 JavaScript 引擎的版本


應用:Java API 範例集

Swing 視窗程式設計
播放 WAVE 和 MP3 音樂
播放 MIDI 音樂
壓縮與解壓縮 ZIP 檔案
使用 SQLite 資料庫
存取剪貼簿文字


附錄

JavaScript 程式設計錦囊


站點

MDN Rhino(頁面已撤除)
MDN JavaScript reference
MDN Standard built-in objects
RingoJS


在 JavaScript 使用 Java API


AAA
BBB
CCC

importPackage() 可以載入套件,就像 import java.util.* 一樣。但預設為不載入任何套件,所以就算是 java.lang.* 的類別也要自己載入。

只想載入某個類別的話,用 importClass()


調用第三方套件

以「羅技 G 系列開發者實驗室」開放下載的「LED 照明 SDK」為例:


java -cp .;logiled.jar;rhino.jar org.mozilla.javascript.tools.shell.Main *.js
若外部 SDK 的套件名稱,不是 com、java、javax、jdk、org、sun 開頭的話,必須用 Packages 開頭,例如 importPackage(Packages.io.github.twideem)。


在 JavaScript 使用 Java 的資料型態


true
127
40
90
ABCDEFGHIJKLMNOPQRSTUVWXYZ

注意!JavaScript 自己也有 Array、Booelan、Date、Math、Number、Object、String 物件,所以要用 Java 的同名類別時,必須用完整套件名稱如 java.lang.String,不要用 importPackage(套件),因為名稱衝突時優先調用 JavaScript 的同名物件,等於沒載入套件。

雖然 JavaScript 沒有 Byte、Character、Double、Float、Integer,但只要是資料型態的類別,一律使用完整套件名稱,保持一致性,可讀性較高,是值得採用的編碼風格。

更極端的編碼風格,是一律不載入 java.lang 套件,只要是這套件的類別,一律使用完整名稱。


調用 Java API 傳入 Java 的類別時

在調用 Java API 時,若需要傳入的參數是類別,應該把傳入的變數宣告為 Java 的物件,而不是宣告 JavaScript 的物件傳入。例如需要傳入一個 ArrayList,就 let a = new ArrayList() 然後傳進去,不要宣告 let a = [] 再想方設法丟進去,以免發生自動轉型的問題,程式跑起來性能也不理想。

其實,就像東西買回來應該先好好看一下說明書,寫 Rhino 程式調用 Java API 裡的東西,也應該先查閱 Java API Specification,看有沒有傳回值?需要傳入哪些物件?掌握好一切,好好去張羅,用起來比較不會遇到問題!

查閱 Java API Specification、了解總共需要用到哪些物件、一個個宣告出來用…這雖然需要花費時間,但往往比直接寫 JavaScript 程式碼去碰運氣還要快。碰運氣看 JavaScript 的 Array、String、Number、Objuet 能不能丟進去用,不能時就轉型來轉型去,以我過來人的經驗,反而更花時間,更別說這樣的程式性能會很糟糕。

我對 JavaScript 和 Java 兩邊自動轉型的原則是:「盡量讓 Java 物件轉為 JavaScript 物件,這點 Rhino 做得很出色。但盡量不要讓 JavaScript 物件轉 Java 物件,這部分 Rhino 跑得很慢,而且很多意料之外的狀況。」


覆載物件方法與實作介面功能


以覆載 Thread 物件的 run() 方法為例


Hello!


以實作 Runnable 介面的 run() 功能為例


Hello!
基於 Lambda 的特性,介面只有一個功能需要實作的話,可以省略介面名稱:


Hello!


多執行緒

假設有個播放音樂的函式,我們不想等音樂播完再執行後面的程式,而是一邊播放一邊執行其它的程式,這時要寫多執行緒!


用 Java API 的 Thread 物件


用 Rhino 內建函式 spawn()


補充

像 playMusic() 這樣播放背景音樂的函式,可以把執行緒寫在函式裡面:

Thread.run()

spawn()


在 *.js 中載入其它 *.js

load('檔名.js')


最頂層物件

網頁瀏覽器的最頂層物件是 window,而 Rhino 則是 global

既然最頂層物件不同,能呼叫的內建函式1 也不盡相同,例如沒有 setTimeout() 可用。

內建函式能否執行,要看執行的環境。在 interactive mode 所有功能都用,而直譯器執行 *.js 有些功能無法作用,例如 readline()。

其實 Java API 能做到的,盡量用 API 去完成,往往比用內建函式省事~


列出 JavaScript 物件的屬性和方法


Rhino 所有內建函式都在 global 裡,這個程式可以隨時列出來看。

要看 API 文件的詳細說明,可查 org.mozilla.javascript.tools.shell.Global 類別。


Rhino 內建屬性和函式

arguments
environment
history
help()
defineClass(Packages.className)
deserialize(fileName)
gc()
load([*.js, ...])
loadClass(Packages.className)
print([expr ...])
readFile(path [, characterCoding])
readUrl(url [, characterCoding])
runCommand(commandName, [arg, ...] [options])
seal(object)
serialize(object, fileName)
spawn(function or script)
sync(function)
quit()
version([number])


arguments

這不是函式物件的 arguments,而是 global 物件的,用來取得命令列的參數。

範例:


java -cp .;rhino.jar org.mozilla.javascript.tools.shell.Main *.js 123
123
要拖曳檔案給 JavaScript 程式碼,可在執行 *.js 的批次檔用 %1 到 %9 當作參數:

java -cp .;rhino.jar org.mozilla.javascript.tools.shell.Main *.js %1
再把檔案拖曳到批次檔即可。


history

紀錄 Shell 輸入過的指令。


defineClass()

用來載入繼承自 ScriptableObject 的 Java 類別。

這種類別更符合 JavaScript 物件的特性,能用 JavaScript 的語法糖操作 Java 物件。但無法用「new Packages.類別名稱()」建立,所以用「defineClass(Packages.類別名稱)」掛載進來。

Sample.java

main.js


Hello!
Bye~


loadClass()

用來執行實作 Script 介面的類別。

它並不是載入類別來用,而是直接執行寫在 exec() 裡面的程式。

當 JavaScript 無法正常使用 Java API 的功能,不得不用 Java 設計成 *.class 來調用時,這機制是很棒的選擇!

Sample.java

main.js


Hello!


其它 Rhino 的不標準語法


var 在全域範疇的影響


undefined
[object global]
123
123

Rhino 將變數放在全域物件的 prototype,這設計或多或少降低 var 對全域的汙染。但 var 的其它容錯特性還是會帶來副作用,所以 let 還是很重要。


let 和 const 的實作並不標準

Rhino 的 letconst 並未完全實作 ECMAScript 6 規範的功能,所以全面改用 let 和 const 取代 var 的話,要注意與其它 JavaScript 引擎表現起來不盡相同的問題!

可以把 let 用在 for() 的計數器和流程控制的 {} 裡面就好,用在其它場合的話,不要指望它能和網頁瀏覽器的 let 一樣,完全解決 var 的弊端:「因為 Rhino 的 let 像是拿 var 改出來湊用的,在全域範疇使用,依然會將變數放入 global 物件。」而 const 也有問題,修改變數不會報錯,只是資料並未修改而已,跟 var 出來的變數重複宣告不會報錯一樣。


function 物件裡面只有一行敘述的話,可以省略 {} 括號


Hello!


在 Java 寫 JavaScript 程式


Hello, JavaScript!
org.mozilla.javascript 套件底下,有各式類別可以組成 JavaScript,進行更細部的設計。


使用新版 Rhino 在 Java 寫 JavaScript 程式

javac -cp rhino.jar *.java


從 Java 傳資料給 JavaScript 程式


寫在全域範疇


123


寫在區域範疇(避免汙染最頂層物件)


456


在 Java 載入 *.js 檔案


script.js


Main.java


Hello, JavaScript!


直接用 JDK 執行 *.js 檔

JDK 提供 jrunscript.exe 程式,有 JavaScript 直譯器的功能!


script.js


命令提示字元

jrunscript script.js
Hello, JavaScript!


在 JDK 6/7 將 *.js 編譯成 *.class

雖然 Java SE 6/7 內建 Rhino 引擎,但只打包部分功能,並未提供所有功能。所以要將 *.js 編譯成 *.class,得自行下載 Rhino,才有 org.mozilla.javascript.tools.jsc。


進入 JDK 內建的 Interactive mode

執行 jrunscript.exe 時不加參數,會進入 interactive mode,一個能用 JavaScript 語言操作系統的環境:

C:\Users\User>jrunscript
js> _


如何取得 Java 內建 JavaScript 引擎的版本

jrunscript -q


Swing 視窗程式設計


視窗、按鈕、事件


插圖


呼叫另一個視窗


插圖 插圖


啟動視窗小秘訣

由於 Java 寫的應用程式是透過 java.exe 指令執行,所以視窗應用程式啟動時,會帶一個命令提示字元,相當礙眼。

請善用 CMD /C START "" 指令,可以在執行完指令後,自動結束命令字元視窗:

CMD /C START "" javaw -cp .;"%JAVA_HOME%\lib\rhino.jar" org.mozilla.javascript.tools.shell.Main -version 200 -encoding UTF-8 *.js
在其它表述語言,通常直接建立捷徑,輸入 CMD /C START "" ... 即可。但 java.exe 掛載 rhino.jar 的參數太複雜,裡面有下給參數的參數,還得寫 %"%"%"%" 這種符號表示 "",建議還是乖乖寫在批次檔執行吧!點兩下批次檔,照樣會自動關閉命令提示字元視窗,不但不會比較難用,反而比捷徑更容易修改、維護。


播放 WAVE 和 MP3 音樂


這程式可以播放 WAVE 格式音樂,同樣的程式只要到:

http://sourceforge.net/projects/mp3pl/files/lib/

下載 jl1.0.1.jar、mp3spi1.9.5.jar、tritonus_share-0.3.6.jar 三個套件,用 -cp 參數掛載進去:

java -cp .;jl1.0.1.jar;mp3spi1.9.5.jar;tritonus_share-0.3.6.jar;rhino.jar org.mozilla.javascript.tools.shell.Main *.js
就具備播放 MP3 格式的能力了!不用修改程式碼。


播放 MIDI 音樂


壓縮與解壓縮 ZIP 檔案


壓縮

將 archive 資料夾壓縮為 archive.zip:


解壓縮


使用 SQLite 資料庫

請到 http://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/ 下載 SQLite JDBC,然後用 -cp 參數掛載,就能使用 SQL 存取資料:


java -cp .;sqlite-jdbc-3.34.0.jar;rhino.jar org.mozilla.javascript.tools.shell.Main *.js
哈囉


存取剪貼簿文字


JavaScript 程式設計錦囊


Value object


ABC
1111011
FALSE
333,222,111
密碼有效


Higher-order function


111 0 111,222,333
222 1 111,222,333
333 2 111,222,333

222
444
666

112
223
334

777


Destructuring assignment


123
456
789


Top-level functions

善用以下函式:

eval(string)
parseInt(string [,radix])
parseFloat(string)
isFinite(value)
encodeURI(string)
decodeURI(string)
encodeURIComponent(string)
decodeURIComponent(string)

建議用 Number.isNaN() 取代 isNaN()。
建議用 String.charCodeAt() 取代 escape()。
建議用 String.fromCharCode() 取代 unescape()。


Array.prototype.join() 和 String.prototype.split() 的妙用


1 2 3 4 5 6 7 8 9
a
b
c
d
e


Array.prototype.slice.call() 和 [].slice.call() 的差別

前者不用建立 Array 物件,直接調用 slice 方法。

後者等於 new 一個 Array 物件,然後呼叫物件的 slice 方法。

雖然前者不用 new 所以效能更好,還可避免建立大量的垃圾,但程式碼落落長,沒有後者簡短,所以只建立一個物件的話,大多數人都是用後者。

在迴圈會產生大量的物件,就改用前者吧!不像網頁瀏覽器的 JavaScript 引擎(例如 SpiderMonkey),會最佳化編譯再執行,兩者執行起來差別很小,Rhino 就只是直譯而已~


Rhino 1.7.13 仍不支援的新語法替代方案

Default parameter

舊做法是用 if(!參數) 參數=初始值; 實現類似的需求。

但這判斷不夠精準,容易出包,建議寫成 if(參數==undefined)

如果函式的功能只是傳回參數,也可以寫成 return 參數||預設值;

Rest parameter

舊做法是用 arguments 實現類似的需求。

Template literal

沒有類似的舊做法。

有個沒這語法方便,但還算不錯用的「格式化字串」可以自己做:

寫成函式


AAA BBB CCC
AAA {1} {2}

如結果所示,沒有對應的參數時顯示 {1} 和 {2}。若不喜歡這樣的設計,可以刪掉 || match,沒有對應的參數會顯示 undefined。

寫在 String 物件


AAA BBB CCC

引用 Java API

Java 的 java.util.Formatter 提供功能更完整的格式化字串功能!如果要直接輸出格式化字串的,還可以用 System.out.printf()


Nashorn 二三事

jjs 或 jrunscript

建議使用 jjs,程式碼有錯誤時,會剖析語法,指出錯誤原因與程式碼位置。

jrunscript 只會拋出例外,不方便除錯。

使用 ECMAScript 6 語法

jjs main.js --language=es6
Java 11 的 Nashorn 支援 `` 符號的字串,能表示多行字串,以及用 ${變數名稱或運算式} 將程式崁在字串裡。還支援預設參數。

Java 8 的 Nashorn 不支援上述功能。

Nashorn 不支援 Destructuring assignment,Rhino 卻有支援,只能說 Java 底下的 JavaScript 引擎沒一個好的~

如何 import 套件

load('nashorn:mozilla_compat.js'),就能使用 Rhino 的 importPackage()。

關於 JFrame

不需要 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE),關閉視窗圖示就能結束視窗。但!還是要加!不然 jjs 的行程會殘留在電腦裡,不會隨視窗結束而關閉!

需要用 frame.repaint() 重繪視窗元件,才能在載入視窗時顯示 JFrame.add() 的元件。