改进翻译

Kotlin 中调用 JavaScript

Kotlin 最初被设计为能够与 Java 平台轻松互操作。它将 Java 类视为 Kotlin 类,并且 Java 也将 Kotlin 类视为 Java 类。

但是,JavaScript 是一种动态类型语言,这意味着它不会在编译期检测类型。可以通过动态类型在 Kotlin 中自由地与 JavaScript 交流。如果想要使用 Kotlin 类型系统的全部威力,可以为 JavaScript 库创建 Kotlin 编译器与周边工具可理解的外部声明。

An experimental tool to automatically create Kotlin external declarations for npm dependencies which provide type definitions (TypeScript / d.ts) called Dukat is also available.

内联 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"

Note that invoking js() returns a result of type dynamic, which provides no type safety at compile time.

external 修饰符

要告诉 Kotlin 某个声明是用纯 JavaScript 编写的,你应该用 external 修饰符来标记它。 当编译器看到这样的声明时,它假定相应类、函数或属性的实现是由外部提供的(由开发人员或者通过 npm 依赖项),因此不会尝试从声明中生成任何 JavaScript 代码。This is also why external declarations can't have a 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 修饰符只允许在包级声明中使用。 你不能声明一个非 external 类的 external 成员。

声明类的(静态)成员

在 JavaScript 中,你可以在原型或者类本身上定义成员:

function MyClass() { …… }
MyClass.sharedMember = function() { /* 实现 */ };
MyClass.prototype.ownMember = function() { /* 实现 */ };

Kotlin 中没有这样的语法。然而,在 Kotlin 中我们有伴生(companion)对象。Kotlin 以特殊的方式处理 external 类的伴生对象:替代期待一个对象的是,它假定伴生对象的成员就是该类自身的成员。可以这样描述来自上例中的 MyClass

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

    fun ownMember()
}

声明可选参数

If you are writing an external declaration for a JavaScript function which has an optional parameter, use definedExternally. This delegates the generation of the default values to the JavaScript function itself:

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

With this external declaration, you can call myFunWithOptionalArgs with one required argument and two optional arguments, where the default values are calculated by the JavaScript implementation of myFunWithOptionalArgs.

扩展 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 接口

JavaScript 没有接口的概念。当函数期望其参数支持 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 检测的右侧使用。
  • 它们不能作为具体化类型参数传递。
  • 它们不能用在类的字面值表达式(例如 I::class)中。
  • as 转换为外部接口总是成功。 Casting to external interfaces produces the "Unchecked cast to external interface" compile time warning. The warning can be suppressed with the @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") annotation.

    IntelliJ IDEA can also automatically generate the @Suppress annotation. Open the intentions menu via the light bulb icon or Alt-Enter, and click the small arrow next to the "Unchecked cast to external interface" inspection. Here, you can select the suppression scope, and your IDE will add the annotation to your file accordingly.

Casting

In addition to the "unsafe" cast operator as, which throws a ClassCastException in case a cast is not possible, Kotlin/JS also provides unsafeCast<T>(). When using unsafeCast, no type checking is done at all during runtime. For example, consider the following two methods:

fun usingUnsafeCast(s: Any) = s.unsafeCast<String>()
fun usingAsOperator(s: Any) = s as String

They will be compiled accordingly:

function usingUnsafeCast(s) {
    return s;
}

function usingAsOperator(s) {
    var tmp$;
    return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE();
}