いきなりですが具体例を見ていきます
前提として、下記の仕様を満たします。
-
+1
のボタンを押すと回数が増える -
+1
のボタン以外の要素を押すとカウントが0にリセットされる
このコードでは仕様通りの動きをします。
Litを使用して書いています。@state()
で指定したものはreactiveになります
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("my-counter")
export class Counter extends LitElement {
@state() counter = 0;
add(e: Event) {
e.stopPropagation();
this.counter++;
const reset = () => {
console.log(this); // => <my-counter></my-counter>
this.counter = 0;
document.removeEventListener("click", reset);
}
document.addEventListener("click", reset);
}
render() {
return html`
<button @click=${(e: Event) => this.add(e)}>+1</button>
<p>クリックされた回数: ${this.counter}</p>
`;
}
}
一方、次のコードでは仕様通りの動きをしません。
+1
のボタン以外を押してもカウントが0にリセットされません。
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("my-counter")
export class Counter extends LitElement {
@state() counter = 0;
reset() {
console.log(this); // => #document
this.counter = 0;
document.removeEventListener("click", this.reset);
}
add(e: Event) {
e.stopPropagation();
this.counter++;
document.addEventListener("click", this.reset);
}
render() {
return html`
<button @click=${(e: Event) => this.add(e)}>+1</button>
<p>クリックされた回数: ${this.counter}</p>
`;
}
}
add
の関数の中でreset
をアロー関数として定義している場合は仕様通りの動きになっているのに対して、add
関数の外でreset
をClassのメンバ関数として定義している場合は仕様から外れた動きとなっていることがわかります。
関数の定義の仕方によって、this
の参照先は変わってしまいます。
this
の種類
this
の参照先は様々な条件によって変わります。
呼び出し方、実行コンテキスト、メソッドなのかアロー関数なのか、などなど、いくつかのポイントがあります。
この記事の中では、先程の例にあったような(個人的に実務で一番詰まりやすいポイントだと思っている)
- メソッドにおける
this
- アロー関数における
this
について深堀していきます。
メソッドにおけるthis
ざっくりした説明
メソッドにおけるthis
はベースオブジェクトを参照します。
先程の例では、add
はCounter
というクラスのメソッド(メンバ)でした。
この場合、add
におけるthis
はCounter
を指します。
例えば、add
のthis.counter++
はCounter
クラスのcounter
というプロパティにアクセスしていますね。
メソッドにおけるthis
の問題
メソッドにおけるthis
はベースオブジェクトを参照すると述べましたが、ベースオブジェクトは呼び出し方によって変わってしまいます(=this
の参照先は定義時ではなく実行時に決まる)。
もう一度、仕様通り動かない例を見てみましょう。
@customElement("my-counter")
export class Counter extends LitElement {
@state() counter = 0;
reset() {
console.log(this); // => #document
this.counter = 0;
document.removeEventListener("click", this.reset);
}
add(e: Event) {
e.stopPropagation();
this.counter++;
document.addEventListener("click", this.reset);
}
render() {
return html`
<button @click=${(e: Event) => this.add(e)}>+1</button>
<p>クリックされた回数: ${this.counter}</p>
`;
}
}
add
でaddEventListener
するとき、this.reset
を指定しています。
add
というメソッドにおけるベースオブジェクトはCounter
であり、reset
というメソッドを指定していそうです。
しかし、これはreset
というメソッドをメソッドとして呼び出しているわけではなく、プロパティとして扱っています(もしメソッドとして呼び出すのであればthis.reset()
となる)。
プロパティとして扱うことで、ただの関数として呼び出されます。
ただの関数であるため、Counter
がベースオブジェクトとなりません。
メソッドにおけるthis
は実行時によって決まるため、このような挙動となります。
reset
の中でthis.counter=0
としていますが、ベースオブジェクトがCounter
ではないため、カウントの値を適切に0に戻すことができなかったというわけです。
アロー関数におけるthis
ざっくりした説明
メソッドにおけるthis
はベースオブジェクトを参照し、実行時にthis
が何を参照するか決まりました。
一方、アロー関数におけるthis
はベースオブジェクトを参照する点では変わりませんが、実行時ではなく定義時に何を参照するかが決まります。
つまり、アロー関数を定義した時点でのベースオブジェクトを参照することになります。
仕様どおりに動いた例を再び見てみましょう。
@customElement("my-counter")
export class Counter extends LitElement {
@state() counter = 0;
add(e: Event) {
e.stopPropagation();
this.counter++;
const reset = () => {
console.log(this); // => <my-counter></my-counter>
this.counter = 0;
document.removeEventListener("click", reset);
}
document.addEventListener("click", reset);
}
render() {
return html`
<button @click=${(e: Event) => this.add(e)}>+1</button>
<p>クリックされた回数: ${this.counter}</p>
`;
}
}
reset
はアロー関数として定義されています。
アロー関数では、アロー関数の外側のスコープのthis
と同じ参照先となります。
今回の例では、add
の中でthis
を使っているのと同じ参照先となり、適切にcounter
プロパティを参照することができます。
つまりアロー関数を使うしかないってこと?
そんなことはありません。
仕様通り動かなかった例で、bind
を使うだけで仕様通り動くようになります。
@customElement("my-counter")
export class Counter extends LitElement {
@state() counter = 0;
reset() {
console.log(this); // => <my-counter></my-counter>
this.counter = 0;
document.removeEventListener("click", this.reset);
}
add(e: Event) {
e.stopPropagation();
this.counter++;
- document.addEventListener("click", this.reset);
+ document.addEventListener("click", this.reset.bind(this));
}
render() {
return html`
<button @click=${(e: Event) => this.add(e)}>+1</button>
<p>クリックされた回数: ${this.counter}</p>
`;
}
}
bind
メソッドを使うことで、this
を明示的にバインドすることが可能になります。
今回の例では、add
メソッドにおけるthis
でバインドするため、reset
におけるthis
がCounter
を参照するようになります。