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

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

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

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

![Screenshot from 2023-08-30 08-33-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529362/54bb765d-63bf-45dc-188b-53a18224d8f6.png)


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

:::note info
Litを使用して書いています。`@state()`で指定したものはreactiveになります
:::

```typescript:仕様通り動く
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にリセットされません。

```typescript:仕様通り動かない
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`の参照先は定義時ではなく実行時に決まる)。  
もう一度、仕様通り動かない例を見てみましょう。  

```typescript:仕様通り動かない
@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`はベースオブジェクトを参照する点では変わりませんが、実行時ではなく定義時に何を参照するかが決まります。  
つまり、アロー関数を定義した時点でのベースオブジェクトを参照することになります。  

仕様どおりに動いた例を再び見てみましょう。

```typescript:仕様通り動く
@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`を使うだけで仕様通り動くようになります。  
```diff_typescript: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`を参照するようになります。


# 参考記事
- [this - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this)
- [関数とthis · JavaScript Primer #jsprimer](https://jsprimer.net/basic/function-this/)
- [JavaScriptにおける this を完全に理解する](https://qiita.com/kenny_J_7/items/9fd18834c8bc0bb3c576)
- [Function.prototype.bind() - JavaScript - MDN Web Docs - Mozilla](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)