【JavaScript/Node.js】クラスに定義したメソッドをコールバックとして実行した際にコールバック内の this がクラスを参照しない件

JavaScript における`this`がコンテキストによって参照するものが異なるということはこの言語のハマり所としてよく知られていると思います。  
今回は Node.js で クラスに定義した[プロトタイプメソッド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes#Prototype_methods)[^1] を同クラスの別のメソッドでコールバックとして実行した際に、コールバック内の this の挙動が想定通りの挙動にならず悩まされたので対処方法と原因を調査しました。

もし誤りがあればコメント欄などでご指摘いただければ幸いです。

## 前提

クラス記法を使用するため、バージョンは ES6 を想定しています。

## 例

以下の`Sample`クラスを例とします。

```javascript
class Sample {
  constructor(prop) {
    this.prop = prop;
  }

  /**
   * `prop`の値をコンソールに出力する
   */
  printProp() {
    console.log(this.prop);
  }

  /**
   * `printProp`をコールバックとして実行する
   */
  printPropAsCallback() {
    this.executeCallback(this.printProp);
  }

  /**
   * 引数で渡されたコールバック関数を実行する
   */
  executeCallback(callback) {
    callback();
  }
}

const sample = new Sample("Sample Class");

sample.printProp(); // => ”Sample Class”と表示される

sample.printPropAsCallback(); // => TypeError: Cannot read property 'prop' of undefined
```

`Sample`クラスのインスタンスを生成し`printProp`を実行するとコンソールに`prop`の値である`Sample Class`が表示されます。
一方、`printPropAsCallback`を実行した場合、`printProp`が`executeCallback`のコールバックとして実行されて上記と同じく`Sample Class`と表示されることを期待しますが、エラーとなってしまいます。

## 解決方法

解決方法としては以下の 2 つがあります。

1. コールバックとして引数に渡す関数内で参照される`this`を`bind`メソッドで指定する
2. `call`または`apply`メソッドを使用して、コールバックとして渡された関数の`this`を指定して実行する
3. **(3/13 追記)** `printProp`をアロー関数でラップしてコールバックとして渡す。

### 1. コールバックとして引数に渡す関数内で参照される`this`を`bind`メソッドで指定する

1 つ目の解決方法は[`bind()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)を使用して this を指定した上で引数に渡す方法です。  
`bind`メソッドは、任意の関数につなげて実行すると、`bind`の第一引数で指定したオブジェクトを`this`として参照する関数を新たに生成します。

```javascript
class Sample {
  constructor(prop) {
    this.prop = prop;
  }

  /**
   * `prop`の値をコンソールに出力する
   */
  printProp() {
    console.log(this.prop);
  }

  /**
   * `printProp`をコールバックとして実行する
   */
  printPropAsCallback() {
    // this.printProp を this.printProp.bind(this) に変更
    this.executeCallback(this.printProp.bind(this));
  }

  /**
   * 引数で渡されたコールバック関数を実行する
   */
  executeCallback(callback) {
    callback();
  }
}

const sample = new Sample("Sample Class");

sample.printProp(); // => ”Sample Class”と表示される

sample.printPropAsCallback(); // => ”Sample Class”と表示される
```

上記の例では`bind`メソッドの第一引数に`Sample`クラスのインスタンスを指す`this`を渡しています。  
これによって`executeCallback`内で`printProp`がコールバックとして実行された際、`printProp`内部では`this`は`Sample`クラスを指すようになります。

### 2. `call`または`apply`メソッドを使用して、コールバックとして渡された関数の`this`を指定して実行する

2 つ目の解決方法は、[`call()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/call)または[`apply()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)により関数内の this の参照を指定した上でコールバックを実行する方法です。  
`call`および`apply`メソッドは任意の関数につなげて実行すると、`call`/`apply`の第一引数で指定したオブジェクトを`this`の参照として関数を実行します[^2]。

```javascript
class Sample {
  constructor(prop) {
    this.prop = prop;
  }

  /**
   * `prop`の値をコンソールに出力する
   */
  printProp() {
    console.log(this.prop);
  }

  /**
   * `printProp`をコールバックとして実行する
   */
  printPropAsCallback() {
    this.executeCallback(this.printProp);
  }

  /**
   * 引数で渡されたコールバック関数を実行する
   */
  executeCallback(callback) {
    // callback(); を callback.call(this); に変更
    callback.call(this);
  }
}

const sample = new Sample("Sample Class");

sample.printProp(); // => ”Sample Class”と表示される

sample.printPropAsCallback(); // => ”Sample Class”と表示される
```

上記例では`call`メソッドの第一引数に、`bind`の時と同じく`this`を渡しています。  
これによって`executeCallback`関数に渡された関数の内部で this は`Sample`クラスを参照するよう指定されているので、`printPropAsCallback`実行時に`printProp`内で`Sample`クラスのプロパティである`prop`の値を取得して出力します。

### **(3/13 追記)** `printProp`をアロー関数でラップしてコールバックとして渡す。

[@htsign さんのコメント](https://qiita.com/Appseed246/items/355219f001161f2141e9#comment-fb54c5104f91405e2c4f)にある通り、アロー関数にラップして渡す方法でも`this`がクラスを参照するようにすることが可能です。

```javascript
class Sample {
  constructor(prop) {
    this.prop = prop;
  }

  /**
   * `prop`の値をコンソールに出力する
   */
  printProp() {
    console.log(this.prop);
  }

  /**
   * `printProp`をコールバックとして実行する
   */
  printPropAsCallback() {
    // this.printProp を () => this.printProp() に変更
    this.executeCallback(() => this.printProp());
  }

  /**
   * 引数で渡されたコールバック関数を実行する
   */
  executeCallback(callback) {
    callback();
  }
}

const sample = new Sample("Sample Class");

sample.printProp(); // => ”Sample Class”と表示される

sample.printPropAsCallback(); // => ”Sample Class”と表示される
```

## そもそもなぜこの事象が発生するのか

プロトタイプメソッドを直接実行した際にメソッド内の`this`がクラスを参照するため、コールバックとして実行した際にも同じように`this`がクラスを参照してくれるように思えますが、実際の挙動は両者で異なりました。  
原因としては以下の要素が関わっています。

1. 関数の呼び出し方による関数内の`this`の参照
2. strict モードと非 strict モードにおける関数内の`this`の参照

### 1. 関数の呼び出し方による関数内の`this`の参照

JavaScript では、関数呼び出し方によってその関数内の`this`の参照先が決定されます。

以下 [MDN: this - オブジェクトのメソッドとして](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%97%E3%81%A6) より引用

> 関数がオブジェクトのメソッドとして呼び出されるとき、その this にはメソッドが呼び出されたオブジェクトが設定されます。
> 次の例では、o.f() が起動したとき、関数内の this には、o オブジェクトが関連付けられます。
>
> ```javascript
> var o = {
>   prop: 37,
>   f: function() {
>     return this.prop;
>   }
> };
> console.log(o.f()); // logs 37
> ```

本記事の例でいうと、`sample.printProp();`のように、`sample`のメソッドとして`printProp`を直接呼び出した場合は、`printProp`内の`this`は、このメソッドが呼び出されたオブジェクトである`sample`を指します。  
そのため、`printProp`は`sample`のプロパティである`prop`を参照し、その値をコンソールに出力します。

### 2. strict モードと非 strict モードにおける関数内の`this`の参照

一方、通常の関数呼び出し時には`this`の値がセットされません。  
その場合には、非 strict モードの場合はグローバルオブジェクト(ブラウザでは`window`, Node.js では`global`)、strict モードの場合は`undefined`が`this`として設定されます。(以下 [MDN: this - 単純な呼び出し](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this#%E5%8D%98%E7%B4%94%E3%81%AA%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97) より)

> ```javascript
> function f1() {
>   return this;
> }
>
> // ブラウザー上で
> f1() === window; // true
>
> // Node 上で
> f1() === global; // true
> ```

> ```javascript
> function f2() {
>   "use strict"; // strict モードを見てください。
>   return this;
> }
>
> f2() === undefined; // true
> ```

ここで、クラス内部のコードは常に Strict モードで実行されるという仕様があります。(以下[MDN: クラス - プロトタイプと静的メソッドによるボクシング](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes#Boxing_with_prototype_and_static_methods)より)

> this に値が付けられずに静的メソッドまたはプロトタイプメソッドが呼ばれると、this の値はメソッド内で undefined になります。たとえ "use strict" ディレクティブがなくても同じふるまいになります。なぜなら、**class 本体の中のコードは常に Strict モードで実行されるからです**。

本記事の初めの例では、`executeCallback`は引数に渡された関数`callback`を実行していますが、  
`callback`という値は`executeCallback`から見たらただの関数なので、たとえクラス内のメソッドを引数として渡したとしても、渡した関数内の`this`の参照は保持されません。
また、クラス内部のコードに常に strict モードが適用されることから、`callback`実行時に`this`の参照は`undefined`となります。

つまり、`printPropAsCallback`実行時に`executeCallback`内部でコールバックとして`printProp`が実行された際には、`printProp`内部の`this`は`undefined`となっているため、`undefined`のプロパティを参照しようとしてエラーが起きたということになります。

また、クラスを`prototype`で定義した場合、非 strict モードでは`printProp`内部の`this`はグローバルオブジェクトになります。

```javascript
// Node.jsで実行

function Sample(prop) {
  // コンストラクタ
  // 引数で渡された値をpropに設定する
  this.prop = prop;
}

Sample.prototype.printProp = function() {
  console.log(this.prop);
};

Sample.prototype.executeCallback = function(callback) {
  callback();
};

Sample.prototype.printPropAsCallback = function() {
  this.executeCallback(this.printProp);
};

const sample = new Sample("Sample Class");

sample.printProp(); // => ”Sample Class”と表示される

global.prop = "Global Object"; // グローバルオブジェクトにpropプロパティを定義
sample.printPropAsCallback(); // => "Global Object"と表示される
```

## まとめ

- 関数内の`this`の参照は実行時のコンテクストによって決定する
  - 実行される関数がオブジェクトのメソッドである場合、実行されたオブジェクトが`this`になる
  - そうでない場合には、strict モードと非 strict モードで動作が異なる。
      - 非 strict モードではグローバルオブジェクト (ブラウザでは`window`, Node.js では`global`)
      - strict モードでは`undefined`
- プロトタイプメソッドをコールバックとして実行する際には`this`の参照は保持されない
- そのため以下の方法で関数内の`this`の値を明示的に指定する必要がある
  - コールバックとして渡す関数の`this`を`bind`メソッドで指定する
  - `call`, `apply`を使用して関数内の`this`を指定した上でコールバックを実行する

思ったより非常に高度な内容だったので、繰り返しにはなりますが、誤りがありましたらご指摘いただければ幸いです。
長くなりましたが、ここまで読んでくださりありがとうございました。

[^1]: Java などのオブジェクト指向言語におけるインスタンスメソッド。JS のクラス記法は`prototype`の糖衣構文なので、クラス記法で定義したメソッドもプロトタイプメソッドである。
[^2]: `call`と`apply`の違いは第 2 引数以降の形式が異なる点です。両メソッドは第 2 引数以降の値を関数の引数として実行します。本サンプルでは引数を使用しないためどちらの関数を使用しても結果は変わりません。ですが`bind`は実行時に新たな関数を生成するので、前述の 2 つの関数とは用途が異なります。