# 在 Kotlin 中使用 JavaScript 代码 Kotlin 设计时最优先重视与 Java 平台交互的问题: Kotlin 代码可以将 Java 类当作 Kotlin 类来使用, Java 代码也可以将 Kotlin 类当作 Java 类来使用. 然而, JavaScript 是一种动态类型的语言, 也就是说它在编译时刻不做类型检查. 通过 [动态类型](dynamic-type.html), 在 Kotlin 中你可以自由地与 JavaScript 交互, 如果你希望完全发挥 Kotlin 类型系统的能力, 你可以为 JavaScript 库创建外部声明, Kotlin 编译器及相关工具能够正确处理这些外部声明. ## 内联 JavaScript 使用 [js()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/js.html) 函数, 你可以将 JavaScript 代码内联到你的 Kotlin 代码中: ```KOTLIN fun jsTypeOf(o: Any): String { return js("typeof o") } ``` JavaScript 代码内联功能完全支持 [ES2015 的全部功能](js-project-setup.html#support-for-es2015-features), 包括: * `const` 和 `let` 变量声明 * ES 类 * 生成器(Generator) * Lambda ([箭头函数](whatsnew21.html#support-for-generating-es2015-arrow-functions)) * 展开(Spread)和剩余(Rest) 操作符 * 模版字符串 由于 `js` 函数的参数会在编译时解析, 然后原样的("as-is")翻译为 JavaScript 代码, 因此参数必须是字符串常数. 所以, 以下代码是不正确的: ```KOTLIN fun jsTypeOf(o: Any): String { return js(getTypeof() + " o") // 错误: 参数必须是字符串常数 // 编译器无法计算字符串拼接操作 } fun getTypeof() = "typeof" ``` 相反, 例如要内联剩余(Rest) 操作符, 请使用字符串常数: ```KOTLIN fun runSumExample() { val sum = js("(...nums) => nums.reduce((a, b) => a + b, 0)") println(sum(1, 2, 3, 4)) } ``` > **Note:** > 调用 `js()` 返回 [dynamic](dynamic-type.html) 类型的值, 这个类型在编译时不保证任何类型安全性. ## `external` 修饰符 你可以对某个声明使用 `external` 修饰符, 来告诉 Kotlin 它是由纯 JavaScript 编写的. 编译器看到这样的声明后, 它会假定对应的类, 函数, 或属性的实现, 会由外部提供 (由开发者提供, 或由 [npm 依赖项](js-project-setup.html#npm-dependencies)提供), 因此它不会为这个声明生成 JavaScript 代码. 由于同样的原因, `external` 声明不能带有 body 部. 比如: ```KOTLIN 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 中, 成员函数可以定义在 prototype 上, 也可以定义在类上: ```JAVASCRIPT function MyClass() { ... } MyClass.sharedMember = function() { /* 实现代码 */ }; MyClass.prototype.ownMember = function() { /* 实现代码 */ }; ``` 在 Kotlin 中没有这样的语法. 但是, 在 Kotlin 中有 [同伴(companion)](object-declarations.html#companion-objects) 对象. Kotlin 以特殊的方式处理 `external` 类的同伴对象: 它不是期待一个对象, 而是假设同伴对象的成员在 JavaScript 中是定义在类上的成员函数. 上例中的 `MyClass`, 在 Kotlin 中可以写为: ```KOTLIN external class MyClass { companion object { fun sharedMember() } fun ownMember() } ``` ### 声明带默认值的参数 如果一个 JavaScript 函数包含带默认值的参数, 那么编写外部声明时请使用 `definedExternally`. 这个设置会将参数默认值的生成委托给 JavaScript 函数自身: ```KOTLIN external fun myFunWithOptionalArgs( x: Int, y: String = definedExternally, z: String = definedExternally ) ``` 通过这样的外部声明, 你就可以使用一个必须参数和两个可选参数来调用 `myFunWithOptionalArgs` 函数, 其中, 可选参数的默认值将由 `myFunWithOptionalArgs` 函数的 JavaScript 实现负责计算. ### 扩展 JavaScript 类 你可以很容易地扩展 JavaScript 类, 就好像它们是 Kotlin 类一样. 你只需要定义一个 `external open` 类, 然后通过一个非 `external` 类来扩展它. 比如: ```KOTLIN 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` 类. ### external 接口 JavaScript 没有接口的概念. 如果一个函数要求它的参数支持 `foo` 和 `bar` 两个方法, 你只需要传递一个确实带有这些方法的对象. 在严格检查类型的 Kotlin 语言中, 你可以使用接口来表达这种概念: ```KOTLIN external interface HasFooAndBar { fun foo() fun bar() } external fun myFunction(p: HasFooAndBar) ``` `external` 接口的另一种典型的使用场景, 是用来描述配置信息对象. 比如: ```KOTLIN 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` 检查语句的右侧. * 它们不可以用作实体化的类型参数(reified type argument). * 它们不可以用在类的字面值表达式中(比如 `I::class`). * 使用 `as` 将对象转换为 `external` 接口, 永远会成功. 转换为 `external` 接口会产生编译期警告 "Unchecked cast to external interface". 要消除这个警告, 可以使用 `@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")` 注解. IntelliJ IDEA 也能够自动生成 `@Suppress` 注解. 方法是在编辑器内点击灯泡图标, 或按下 Alt-Enter 快捷键, 打开 intentions 菜单, 然后点击代码检查信息 "Unchecked cast to external interface" 旁的小箭头. 在这里, 你可以选择对这个警告进行屏蔽的适用范围, 然后 IDE 会在你的源代码文件中添加对应的注解. ### 类型转换 ["不安全的" 类型转换操作符](typecasts.html#unsafe-cast-operator) `as` 在转换失败时会抛出 `ClassCastException` 异常, 除此之外, Kotlin/JS 还提供了 [unsafeCast<T>()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/unsafe-cast.html). 如果使用 `unsafeCast`, 在运行时 完全不进行任何类型检查. 比如, 对于下面两个方法: ```KOTLIN fun usingUnsafeCast(s: Any) = s.unsafeCast() fun usingAsOperator(s: Any) = s as String ``` 对应的编译结果分别是: ```JAVASCRIPT function usingUnsafeCast(s) { return s; } function usingAsOperator(s) { var tmp$; return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE(); } ``` ## 相等判断 与其他平台相比, Kotlin/JS 的相等判断语义有所不同. 在 Kotlin/JS 中, Kotlin [引用相等](equality.html#referential-equality) 操作符 (`===`) 永远会翻译为 JavaScript 的 [严格相等](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality) 操作符 (`===`). JavaScript `===` 操作符不仅检查两个值相等, 而且检查这两个值的类型也相等: ```KOTLIN 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](js-to-kotlin-interop.html#kotlin-types-in-javascript) 在运行期都使用 JavaScript 类型 `Number` 表达. 因此, 这 5 种类型的值是无法区分的: ```KOTLIN fun main() { println(1.0 as Any === 1 as Any) // 在 Kotlin/JS 平台, 输出结果为 'true' // 在其他平台, 输出结果为 'false' } ``` > **Tip:** > 关于 Kotlin 中的相等判断, 更多详情请参见 [相等判断](equality.html) 文档.