Kotlin/JS TIPS - this の扱い
概要
JavaScript の関数中の this は関数を保持しているオブジェクトによって決定されます。 この振る舞いが Kotlin/JS を使う上での問題になります。 this を扱ういくつかの方法を説明します。 最適な方法とは言えないので、より良い方法があれば教えてください。
ソースコードは前回の記事の続きになっています。 詳しくは前回の記事を参照してください。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- 算出プロパティとウォッチャ - Vue.js
- Vue.js の算出プロパティ
解説
this の問題点
以下の JavaScript のコードは 算出プロパティとウォッチャ - Vue.js から引用したコードに少しだけ手を加えたコードである。firstName
と lastName
に値を設定すると fullName
が算出されるサンプルである。
var vm = new Vue({ el: '#example', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } }) vm.firstName = 'Taro' vm.lastName = 'Yamada'
この処理を Kotlin/JS で書いてみる。SampleClient.kt
を以下に変更する。
@JsNonModule @JsModule("vue") external open class Vue(option: VueOption) external interface VueOption : Json { var el: Any var data: Json var computed: Json } fun <T : Json> Json(): T = js("({})") fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init)) external interface Model : Json { var firstName: String var lastName: String } fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = { // this は Vue クラスのインスタンス this as Model // firstName, lastName を参照したいのでキャスト "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
sample-client/index.html
には以下を記述して、ブラウザで開いてみる。
<div id="example">{{ fullName }}</div>
ブラウザに表示される内容は undefined undefined
である。
残念ながら上記のコードではやりたいことは行えない。
${this.firstName} ${this.lastName}
の this
に問題がある。
JavaScript と Kotlin での this の違い
先の例では、JavaScript の関数中の this
は関数を保持しているオブジェクトを示している。
実行時に決定されるので、this
は Vue クラスのインスタンスを示すことになる。
一方 Kotlin の例では、Kotlin のラムダ中の this
はそのラムダの外側のスコープの this
になっている。
つまり、Json クラスのインスタンスを示すことになる。
レシーバ付き関数リテラル
実行時に this
を決定したいのであれば、レシーバ付き関数リテラルを使用する。
SampleClient.kt
を以下に変更する。
fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = fun Model.(): String { // this は実行時に決定される return "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
external との併用における問題点
external interface
を使うことで、firstName =
, lastName =
の様な簡潔な記述を実現している。
fullName
も同様に簡潔な記述にするために以下のコードを追加したくなる。
external interface Computed : Json { var fullName: Model.() -> String }
残念ながらコンパイルエラーとなる。
external
宣言中ではレシーバ付き関数型は禁止されているとエラーメッセージが表示される。
(禁止している理由を知っている方がいたら教えてください。)
external との併用
レシーバ付き関数リテラルを使えば this
の扱いを動的に変更できるが、
external
宣言と併せて使うことができない。
external
宣言を使う場合にはレシーバなしの関数リテラルにする必要がある。
例えば、以下のとおり。
external interface Computed : Json { var fullName: () -> String }
そうすると最初の問題に戻ってしまう。
回避するための苦肉の策として js("this")
を使う方法がある。
コード生成時に JavaScript の this
をそのまま出力させる。
以下がその例である。
inline fun <T> thisAs(): T = js("this") fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json<Computed> { // this は Json クラスのインスタンス fullName = { val self = thisAs<Model>() // self は実行時に決定される "${self.firstName} ${self.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
inline
を付けないと thisAs
関数が生成され、thisAs
関数を保持しているオブジェクトが存在しないために thisAs
関数の結果は undefined
になる。