0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのthisと少し仲良くなる話

Last updated at Posted at 2023-09-02

いきなりですが具体例を見ていきます

前提として、下記の仕様を満たします。

  • +1のボタンを押すと回数が増える
  • +1のボタン以外の要素を押すとカウントが0にリセットされる

Screenshot from 2023-08-30 08-33-33.png

このコードでは仕様通りの動きをします。

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はベースオブジェクトを参照します。
先程の例では、addCounterというクラスのメソッド(メンバ)でした。
この場合、addにおけるthisCounterを指します。
例えば、addthis.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>
    `;
  }
}

addaddEventListenerするとき、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を使うだけで仕様通り動くようになります。

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におけるthisCounterを参照するようになります。

参考記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?