跳至主要内容

從 Kotlin 使用 JavaScript 程式碼

Kotlin 最初的設計目標是易於與 Java 平台互操作:它將 Java 類別視為 Kotlin 類別,而 Java 將 Kotlin 類別視為 Java 類別。

然而,JavaScript 是一種動態類型語言,這意味著它不會在編譯時檢查類型。 你可以通過 dynamic 類型自由地從 Kotlin 與 JavaScript 交互。 如果你想使用 Kotlin 類型系統的全部功能,你可以為 JavaScript 函式庫建立外部宣告(external declarations),這些宣告將被 Kotlin 編譯器和周圍的工具所理解。

行內 JavaScript(Inline JavaScript)

你可以使用 js() 函數將 JavaScript 程式碼內嵌到 Kotlin 程式碼中。 例如:

fun jsTypeOf(o: Any): String {
return js("typeof o")
}

由於 js 的參數在編譯時被解析並「原樣」翻譯成 JavaScript 程式碼,因此它必須是字串常數。 所以,以下程式碼是不正確的:

fun jsTypeOf(o: Any): String {
return js(getTypeof() + " o") // 此處報告錯誤
}

fun getTypeof() = "typeof"
備註

由於 JavaScript 程式碼由 Kotlin 編譯器解析,因此可能不支援所有 ECMAScript 功能。 在這種情況下,你可能會遇到編譯錯誤。

請注意,調用 js() 會返回 dynamic 類型的结果,該類型在編譯時不提供任何類型安全。

external 修飾符

要告訴 Kotlin 某個宣告(declaration)是用純 JavaScript 撰寫的,你應該使用 external 修飾符標記它。 當編譯器看到這樣的宣告時,它會假定相應類別、函數或屬性的實現是從外部提供的(由開發人員或通過 npm 依賴項),因此不會嘗試從宣告中產生任何 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

// 等等
}

external val window: Window

請注意,external 修飾符會被嵌套宣告繼承。 這就是為什麼在 Node 類別的示例中,成員函數和屬性之前沒有 external 修飾符。

external 修飾符僅允許在套件級別宣告(package-level declarations)上使用。 你不能宣告一個非 external 類別的 external 成員。

宣告類別的(靜態)成員

在 JavaScript 中,你可以在原型(prototype)或類別本身上定義成員:

function MyClass() { ... }
MyClass.sharedMember = function() { /* implementation */ };
MyClass.prototype.ownMember = function() { /* implementation */ };

Kotlin 中沒有這樣的語法。 但是,在 Kotlin 中,我們有 companion 物件。 Kotlin 以特殊的方式處理 external 類別的伴生物件(companion objects):它不是期望一個物件,而是假定伴生物件的成員是類別本身的成員。 上面示例中的 MyClass 可以描述如下:

external class MyClass {
companion object {
fun sharedMember()
}

fun ownMember()
}

宣告可選參數

如果你正在為具有可選參數的 JavaScript 函數編寫外部宣告,請使用 definedExternally。 這會將預設值的產生委託給 JavaScript 函數本身:

external fun myFunWithOptionalArgs(
x: Int,
y: String = definedExternally,
z: String = definedExternally
)

使用此外部宣告,你可以使用一個必需參數和兩個可選參數調用 myFunWithOptionalArgs,其中預設值由 myFunWithOptionalArgs 的 JavaScript 實現計算。

擴展 JavaScript 類別

你可以像擴展 Kotlin 類別一樣輕鬆地擴展 JavaScript 類別。 只需定義一個 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")
}
}

有一些限制:

  • 當外部基底類別的函數被簽名(signature)重載時,你不能在派生類別中覆蓋它。
  • 你不能覆蓋具有預設參數的函數。
  • 非外部類別不能被外部類別擴展。

external 介面(external interfaces)

JavaScript 沒有介面(interface)的概念。 當一個函數期望它的參數支援兩個方法 foobar 時,你只需傳入一個實際具有這些方法的物件。

你可以使用介面在靜態類型的 Kotlin 中表達這個概念:

external interface HasFooAndBar {
fun foo()

fun bar()
}

external fun myFunction(p: HasFooAndBar)

外部介面的典型用例是描述設定物件。 例如:

external interface JQueryAjaxSettings {
var async: Boolean

var cache: Boolean

var complete: (JQueryXHR, String) `->` Unit

// 等等
}

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 檢查的右側使用。

  • 它們不能作為具體化的類型參數(reified type arguments)傳遞。

  • 它們不能在類別字面量(class literal)表達式中使用(例如 I::class)。

  • as 轉換為外部介面總是成功。 轉換為外部介面會產生「未經檢查的轉換為外部介面(Unchecked cast to external interface)」編譯時警告。 可以使用 @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 注釋來抑制該警告。

    IntelliJ IDEA 也可以自動產生 @Suppress 注釋。 通過燈泡圖示或 Alt-Enter 打開意圖選單,然後點擊「未經檢查的轉換為外部介面(Unchecked cast to external interface)」檢查旁邊的小箭頭。 在這裡,你可以選擇抑制範圍,你的 IDE 會相應地將注釋添加到你的檔案中。

類型轉換(Casts)

除了 "不安全"的轉換運算符 as(如果轉換不可能,它會抛出 ClassCastException)之外,Kotlin/JS 還提供了 unsafeCast<T>()。 使用 unsafeCast 時,在運行時_根本不進行類型檢查_。 例如,考慮以下兩種方法:

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();
}

相等性(Equality)

與其他平台相比,Kotlin/JS 在相等性檢查方面具有特定的語意。

在 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 類型表示。 因此,這五種類型的值是無法區分的:

fun main() {
println(1.0 as Any === 1 as Any)
// 在 Kotlin/JS 上印出 'true'
// 在其他平台上印出 'false'
}
提示

有關 Kotlin 中相等性的更多資訊,請參閱 相等性(Equality) 文件。