Kotlin/JS TIPS - this の扱い

概要

JavaScript の関数中の this は関数を保持しているオブジェクトによって決定されます。 この振る舞いが Kotlin/JS を使う上での問題になります。 this を扱ういくつかの方法を説明します。 最適な方法とは言えないので、より良い方法があれば教えてください。

ソースコードは前回の記事の続きになっています。 詳しくは前回の記事を参照してください。

nosix.hatenablog.com

目次

確認環境

  • IntelliJ IDEA Community 2017.2
  • Kotlin 1.1.3-2
  • Gradle 3.5
    • Groovy 1.4.10

参考情報

解説

this の問題点

以下の JavaScript のコードは 算出プロパティとウォッチャ - Vue.js から引用したコードに少しだけ手を加えたコードである。firstNamelastName に値を設定すると 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") を使う方法がある。 コード生成時に JavaScriptthis をそのまま出力させる。 以下がその例である。

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 になる。

ソースコード

Release 2017-08-01-164107 · nosix/kotlin-js · GitHub