從 Kotlin 使用 JavaScript 程式碼
Kotlin 最初設計的目的是為了方便與 Java 平台互通:它將 Java 類別視為 Kotlin 類別,而 Java 則將 Kotlin 類別視為 Java 類別。
然而,JavaScript 是一種動態型別語言 (dynamically typed language),這意味著它不會在編譯時期 (compile time) 檢查型別。您可以透過 動態型別 (dynamic type) 自由地從 Kotlin 與 JavaScript 進行通訊。如果您想充分利用 Kotlin 型別系統 (type system) 的強大功能,您可以為 JavaScript 函式庫建立外部宣告 (external declarations),這些宣告將被 Kotlin 編譯器和周邊工具鏈 (tooling) 理解。
行內 JavaScript
您可以使用 js()
函式將 JavaScript 程式碼行內 (inline) 嵌入到 Kotlin 程式碼中。 例如:
fun jsTypeOf(o: Any): String {
return js("typeof o")
}
由於 js
的參數在編譯時期被解析並「按原樣 (as-is)」翻譯成 JavaScript 程式碼,因此它必須是一個字串常數 (string constant)。所以,以下程式碼是錯誤的:
fun jsTypeOf(o: Any): String {
return js(getTypeof() + " o") // error reported here
}
fun getTypeof() = "typeof"
NOTE
由於 JavaScript 程式碼是由 Kotlin 編譯器解析的,並非所有 ECMAScript 功能都可能受到支援。
在這種情況下,您可能會遇到編譯錯誤 (compilation errors)。
請注意,呼叫 js()
會回傳一個 dynamic
型別的結果,這在編譯時期不提供任何型別安全 (type safety)。
external 修飾符
為了告訴 Kotlin 某個宣告是以純 JavaScript 編寫的,您應該使用 external
修飾符標記它。當編譯器看到這樣的宣告時,它會假設對應類別、函式或屬性的實作是由外部提供的(由開發人員或透過 npm 依賴 (npm dependency)),因此不會嘗試從該宣告生成任何 JavaScript 程式碼。這也是 external
宣告不能有函式本體 (body) 的原因。例如:
external fun alert(message: Any?): Unit
external class Node {
val firstChild: Node
fun append(child: Node): Node
fun removeChild(child: Node): Node
// etc
}
external val window: Window
請注意,external
修飾符會被巢狀宣告 (nested declarations) 繼承。這就是為什麼在 Node
類別的範例中,成員函式和屬性之前沒有 external
修飾符。
external
修飾符只允許用於套件層級宣告 (package-level declarations)。您不能宣告非 external
類別的 external
成員。
宣告類別的 (靜態) 成員
在 JavaScript 中,您可以在原型 (prototype) 或類別本身上定義成員:
function MyClass() { ... }
MyClass.sharedMember = function() { /* implementation */ };
MyClass.prototype.ownMember = function() { /* implementation */ };
Kotlin 中沒有這樣的語法。然而,在 Kotlin 中我們有 伴生物件 (companion objects)。Kotlin 會以特殊方式處理 external
類別的伴生物件:它不期望物件,而是假設伴生物件的成員是類別本身的成員。上述範例中的 MyClass
可以描述如下:
external class MyClass {
companion object {
fun sharedMember()
}
fun ownMember()
}
宣告選用參數
如果您正在為一個具有選用參數 (optional parameter) 的 JavaScript 函式編寫外部宣告,請使用 definedExternally
。這將預設值 (default values) 的生成委託給 JavaScript 函式本身:
external fun myFunWithOptionalArgs(
x: Int,
y: String = definedExternally,
z: String = definedExternally
)
有了這個外部宣告,您可以呼叫 myFunWithOptionalArgs
,帶有一個必要引數和兩個選用引數,其中預設值由 myFunWithOptionalArgs
的 JavaScript 實作計算。
擴充 JavaScript 類別
您可以輕鬆擴充 JavaScript 類別,就像它們是 Kotlin 類別一樣。只需定義一個 external open
類別,然後由一個非 external
類別擴充它。例如:
open external class Foo {
open fun run()
fun stop()
}
class Bar : Foo() {
override fun run() {
window.alert("Running!")
}
fun restart() {
window.alert("Restarting")
}
}
存在一些限制:
- 當外部基礎類別 (external base class) 的函式透過簽章多載 (overloaded by signature) 時,您不能在衍生類別 (derived class) 中覆寫它。
- 您不能覆寫具有預設引數 (default arguments) 的函式。
- 非外部類別不能被外部類別擴充。
外部介面
JavaScript 沒有介面 (interfaces) 的概念。當一個函式期望其參數支援 foo
和 bar
兩個方法時,您只需傳入一個實際具有這些方法的物件。
您可以使用介面在靜態型別 (statically typed) 的 Kotlin 中表達這個概念:
external interface HasFooAndBar {
fun foo()
fun bar()
}
external fun myFunction(p: HasFooAndBar)
外部介面 (external interfaces) 的典型使用案例是描述設定物件 (settings objects)。例如:
external interface JQueryAjaxSettings {
var async: Boolean
var cache: Boolean
var complete: (JQueryXHR, String) -> Unit
// etc
}
fun JQueryAjaxSettings(): JQueryAjaxSettings = js("{}")
external class JQuery {
companion object {
fun get(settings: JQueryAjaxSettings): JQueryXHR
}
}
fun sendQuery() {
JQuery.get(JQueryAjaxSettings().apply {
complete = { (xhr, data) ->
window.alert("Request complete")
}
})
}
外部介面有一些限制:
它們不能用於
is
檢查的右側 (right-hand side ofis
checks)。它們不能作為實化型別引數 (reified type arguments) 傳遞。
它們不能用於類別字面值表達式 (class literal expressions)(例如
I::class
)。轉型為外部介面 (casts to external interfaces) 總是成功。 轉型為外部介面會產生「未檢查的外部介面轉型 (Unchecked cast to external interface)」編譯時期警告 (compile time warning)。該警告可以使用
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
註解 (annotation) 抑制 (suppressed)。IntelliJ IDEA 也可以自動生成
@Suppress
註解。透過燈泡圖示 (light bulb icon) 或 Alt-Enter 開啟意圖選單 (intentions menu),然後點擊「未檢查的外部介面轉型」檢查旁邊的小箭頭。在這裡,您可以選擇抑制範圍 (suppression scope),您的 IDE 將相應地將註解新增到您的檔案中。
轉型 (Casts)
除了在轉型不可能時會拋出 ClassCastException
的「不安全 (unsafe)」轉型運算子 as
之外,Kotlin/JS 還提供了 unsafeCast<T>()
。使用 unsafeCast
時,在執行時期 (runtime) 根本不進行型別檢查。例如,考慮以下兩種方法:
fun usingUnsafeCast(s: Any) = s.unsafeCast<String>()
fun usingAsOperator(s: Any) = s as String
它們將相應地編譯:
function usingUnsafeCast(s) {
return s;
}
function usingAsOperator(s) {
var tmp$;
return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE();
}
相等性
與其他平台相比,Kotlin/JS 在相等性檢查方面具有特定語意 (particular semantics)。
在 Kotlin/JS 中,Kotlin 參照相等性運算子 (===
) 總是轉換為 JavaScript 嚴格相等性運算子 (===
)。
JavaScript 的 ===
運算子不僅檢查兩個值是否相等,還檢查這兩個值的型別是否相等:
fun main() {
val name = "kotlin"
val value1 = name.substring(0, 1)
val value2 = name.substring(0, 1)
println(if (value1 === value2) "yes" else "no")
// 在 Kotlin/JS 上列印 'yes'
// 在其他平台上列印 'no'
}
此外,在 Kotlin/JS 中,Byte
、Short
、Int
、Float
和 Double
這五種數值型別在執行時期都以 Number
JavaScript 型別表示。因此,這五種類型的值是無法區分 (indistinguishable) 的:
fun main() {
println(1.0 as Any === 1 as Any)
// 在 Kotlin/JS 上列印 'true'
// 在其他平台上列印 'false'
}
TIP
有關 Kotlin 中相等性的更多資訊,請參閱相等性 (Equality) 文件。