TL;DR
thisのスコープは動的で、呼び出し方によって決まる。- コールバックに
thisをバインドする API では、thisパラメータに型を与える。新しい API では動的なthisのバインドを避ける。
内容
this は動的に決まる
this のスコープは、レキシカルスコープではなく動的に決まる(JavaScript の this)。
class C {
vals = [1, 2, 3]
logSquares() {
for (const val of this.vals) {
console.log(val ** 2)
}
}
}const c = new C()
c.logSquares() // OKこのとき裏側では、C.prototype.logSquares を呼び出し、this を c にバインドしている。したがって、メソッドを変数に取り出して呼ぶと実行時エラーになる。
const method = c.logSquares
method() // 実行時エラーcall を使って呼び出すと、this を指定できる。
method.call(c) // OKコールバックの this に型を付ける
コールバックに this をバインドしている場合は、this に型を与えて、正しくバインドされるようにするべきである。
function addKeyListener(el: HTMLElement, listener: (this: HTMLElement, e: KeyboardEvent) => void) {
el.addEventListener("keydown", (e) => listener.call(el, e))
}this パラメータは特別で、追加してもパラメータの数は増えない。
el.addEventListener("keydown", (e) => {
listener(el, e)
// ~ Expected 1 arguments, but got 2
})正しくバインドしていない場合は型エラーになる。
el.addEventListener("keydown", (e) => {
listener(e)
// ~~~~~~~~ The 'this' context of type 'void' is not assignable
// to method's 'this' of type 'HTMLElement'.
})呼び出し側は、型安全に this を参照できる。
addKeyListener(el, function (e) {
console.log(this.innerHTML)
// ^? this: HTMLElement
})アロー関数を使うと this が外側のものに置き換わるが、TypeScript はこの問題も検出する。
class Foo {
registerHandler(el: HTMLElement) {
addKeyListener(el, (e) => {
console.log(this.innerHTML)
// ~~~~~~~~~ Property 'innerHTML' does not exist on type 'Foo'.
})
}
}新しい API では動的な this を避ける
新しく API を作るときは、動的な this のバインドを避ける。ES2022 以降では、クラスフィールドにアロー関数を使える(メソッドには使えない。MDN)。アロー関数は自分の this を持たないが、クラスは this を持つ。そのため、クラスフィールドのアロー関数の中の this は、正しくインスタンスを指す。
class ResetButton {
constructor() {
this.onClick = this.onClick.bind(this)
}
onClick = () => {
alert(`Reset ${this}`)
} // この this は ResetButton を参照
}