7.2.2 从 Kotlin 使用 JavaScript 代码

原文链接: https://kotlinlang.org/docs/js-interop.html

7.2.2 从 Kotlin 使用 JavaScript 代码

Kotlin 最初就是为与 Java 平台轻松互操作而设计的:它把 Java 类视为 Kotlin 类,而 Java 也把 Kotlin 类视为 Java 类。

不过,JavaScript 是动态类型语言,也就是说它不会在编译期检查类型。你可以通过 dynamic 类型自由地与 JavaScript 交互。如果你想利用 Kotlin 类型系统的全部能力,可以为 JavaScript 库创建外部声明,Kotlin 编译器和相关工具链都能理解它们。

内联 JavaScript

你可以使用 js() 函数把 JavaScript 代码内联到 Kotlin 代码中:

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

JavaScript 代码内联完整支持 ES2015 特性,包括:

  • const 和 let 变量声明
  • ES 类
  • 生成器
  • Lambda(箭头函数)
  • 展开与剩余运算符
  • 模板字符串

由于 js 的参数是在编译期解析并"原样"翻译成 JavaScript 代码的,它必须是字符串常量。因此下面的代码不正确:

1
2
3
4
5
6
fun jsTypeOf(o: Any): String {
    return js(getTypeof() + " o") // 错误:实参必须是字符串常量
    // 编译器无法对字符串拼接求值
}

fun getTypeof() = "typeof"

反之,举例来说,要内联剩余运算符,请使用字符串常量:

1
2
3
4
fun runSumExample() {
    val sum = js("(...nums) => nums.reduce((a, b) => a + b, 0)")
    println(sum(1, 2, 3, 4))
}

注意: 调用 js() 会返回 dynamic 类型的结果,它在编译期不提供类型安全。

external 修饰符

要告诉 Kotlin 某个声明是用纯 JavaScript 编写的,你应该用 external 修饰符标记它。当编译器看到这样的声明时,它会假定相应类、函数或属性的实现由外部提供(由开发者提供,或通过 npm 依赖),因此不会尝试从该声明生成任何 JavaScript 代码。这也是 external 声明不能有函数体的原因。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 中,你既可以在原型上定义成员,也可以在类自身上定义成员:

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

Kotlin 中没有这样的语法。不过在 Kotlin 中我们有 companion 对象。Kotlin 对 external 类的伴生对象有特殊处理:它不期望那里有一个对象,而是把伴生对象的成员视为类自身的成员。上面例子中的 MyClass 可以这样描述:

1
2
3
4
5
6
7
external class MyClass {
    companion object {
        fun sharedMember()
    }

    fun ownMember()
}

声明带默认值的参数

如果你要为带有默认值参数的 JavaScript 函数编写外部声明,请使用 definedExternally。它把默认值的生成委托给该 JavaScript 函数本身:

1
2
3
4
5
external fun myFunWithOptionalArgs(
    x: Int,
    y: String = definedExternally,
    z: String = definedExternally
)

有了这个外部声明,你可以用一个必填实参和两个可选实参调用 myFunWithOptionalArgs,其中默认值由 myFunWithOptionalArgs 的 JavaScript 实现计算。

继承 JavaScript 类

你可以像继承 Kotlin 类一样轻松地继承 JavaScript 类。只需定义一个 external open 类,并用一个非 external 类继承它。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
open external class Foo {
    open fun run()
    fun stop()
}

class Bar : Foo() {
    override fun run() {
        window.alert("Running!")
    }

    fun restart() {
        window.alert("Restarting")
    }
}

有一些限制:

  • 当外部基类的某个函数按签名重载时,你无法在派生类中重写它。
  • 你不能重写包含带默认值参数的函数。
  • 非 external 类不能被 external 类继承。

external 接口

JavaScript 没有接口的概念。当一个函数期望其参数支持 foo 和 bar 两个方法时,你只需传入一个实际拥有这些方法的对象。

你可以使用接口在静态类型的 Kotlin 中表达这一概念:

1
2
3
4
5
6
7
external interface HasFooAndBar {
    fun foo()

    fun bar()
}

external fun myFunction(p: HasFooAndBar)

external 接口的一个典型用例是描述设置对象。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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")
        }
    })
}

external 接口有一些限制:

  • 它们不能用在 is 检查的右侧。
  • 它们不能作为具体化类型实参传递。
  • 它们不能用在类字面量表达式中(例如 I::class)。
  • 到 external 接口的 as 转换总是成功。转换到 external 接口会产生"Unchecked cast to external interface"编译期警告。该警告可以用 @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 注解抑制。

IntelliJ IDEA 也可以自动生成 @Suppress 注解。通过灯泡图标或 Alt-Enter 打开意图菜单,然后点击 “Unchecked cast to external interface” 检查旁边的小箭头。在这里你可以选择抑制范围,IDE 会相应地把注解添加到你的文件中。

类型转换

除了在无法转换时抛出 ClassCastException 的“不安全"转换运算符 as 之外,Kotlin/JS 还提供了 unsafeCast<T>()。使用 unsafeCast 时,运行时_完全不进行类型检查_。例如,考虑下面两个方法:

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

它们会被相应编译为:

1
2
3
4
5
6
7
8
function usingUnsafeCast(s) {
    return s;
}

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

相等性

与其他平台相比,Kotlin/JS 在相等性检查方面有特殊的语义。

在 Kotlin/JS 中,Kotlin 的引用相等性运算符(===)总是翻译为 JavaScript 的严格相等运算符(===)。

JavaScript 的 === 运算符不仅检查两个值是否相等,还会检查这两个值的类型是否相同:

1
2
3
4
5
6
7
8
9
fun main() {
    val name = "kotlin"
    val value1 = name.substring(0, 1)
    val value2 = name.substring(0, 1)

    println(value1 === value2)
    // 在 Kotlin/JS 上打印 'true'
    // 在其他平台上打印 'false'
}

此外,在 Kotlin/JS 中,Byte、Short、Int、Float 和 Double 这些数值类型在运行时都用 JavaScript 的 Number 类型表示。因此这五种类型的值无法区分:

1
2
3
4
5
fun main() {
    println(1.0 as Any === 1 as Any)
    // 在 Kotlin/JS 上打印 'true'
    // 在其他平台上打印 'false'
}

提示: 关于 Kotlin 中相等性的更多信息,请参阅相等性文档。