Michi's Tech Blog

一人前のWebエンジニアを目指して

TypeScriptのthisについての忘備録

こんにちは!
スマレジ テックファームのMichiです!

TypeScriptのthisは不思議な動きをすることで有名です。自分も定期的に調べては忘れてしまうので、今回は忘備録として残しておきます。

thisとは?

まず前提として、thisとは何かについて説明しておきます。

thisとは自分自身を表すオブジェクトです。次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult() {
    return this.#age >= 20;
  }
}

const mike = new User("Mike", 26);
console.log(mike.isAdult()); // 結果:true

上記のコードはconsole.logmike.isAdult()の実行結果を出力しています。

isAdult()メソッドの返り値はreturn this.#age >= 20;です。このとき、thisは呼び出し元のオブジェクトになります。

具体的には、今回の例ではmike.isAdult()という形でメソッドを読んでいるので、thismikeになります。mike#age = 26というプロパティを持つUser型のインスタンスなので、return this.#age >= 20;の結果はtrueとなります。

よって、mike.isAdult()の実行結果もtrueとなるわけです。

呼び出し方によってthisの値が変わる?

これだけならば簡単なのですが、そうはいかないのがTypeScriptのthisがややこしいところです。

次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult() {
    return this.#age >= 20;
  }
}

const mike = new User("Mike", 26);
const isAdult = mike.isAdult;

console.log(isAdult()); 
// ランタイムエラー: Cannot read private member from an object whose class did not declare it

先ほどとは違い、mike.isAdultをいったんisAdultに代入してから呼び出しています。

いったん代入しただけで、処理は変えていないので結果は変わらないと思えそうですが、実際はランタイムエラーが発生していまいます。

実は、メソッド記法(オブジェクト.メソッド名()の形)を使わずに関数が呼び出された場合、その中でのthisの値はundefinedになります。*1なぜなら、今回isAdult()はグローバルスコープから呼び出されたので、参照するオブジェクトが存在しないからです。

thisの値がundefinedである以上、 return this.#age >= 20;の結果は当然エラーです。よって、上記のコードを実行しても、ランタイムエラーとなるわけです。

このことからわかるように、thisは関数の呼び出し方によって参照が変わる」という少し厄介な性質を持っています。今回のようなエラーを避けるために、thisを使うオブジェクトのメソッドは原則として、メソッド記法で呼んだ方がよいでしょう。

アロー関数はthisを持たない

ここまで書いてきて、一つ例外があります。それはアロー関数を使用した場合です。

次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult = () => {
    return this.#age >= 20;
  };
}

const mike = new User("Mike", 26);
const isAdult = mike.isAdult;

console.log(isAdult()); // 結果:true

isAdultの定義にアロー関数を使用した以外は、先ほどのコードと同じです。よって、実行結果も同じくランタイムエラーとなりそうな気がします。

ところが、このコードは正しく動作します。これは、「アロー関数は自分自身のthisを持たない」という特殊な性質によるものです。

自分自身のthisを持たないならば、アロー関数はどこからthisの参照を引っ張ってきているのでしょうか?

この場合、アロー関数はthisを外側のスコープから引き継ぎます。上記の例において、isAdultUserクラスのスコープに属しています。よって、このthisは、Userから生成されたインスタンス(オブジェクト)となり、ランタイムエラーとはならないわけです。

アロー関数は通常の関数と異なり、呼び出し方によってthisの参照が変化しません。アロー関数を定義した段階で、thisが何であるかは決まっているのです。

このことからも、普段から通常の関数よりもアロー関数を使うほうがよいといえます。

参考

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus) | 鈴木 僚太 |本 | 通販 | Amazon

*1:JavaScriptのstrictモードがOFFの場合は、thisはグローバルオブジェクトとなります。TypeScriptでは、原則すべてstrictモードで実行されるので、undefinedになると覚えておいて問題ありません。