this は呼び出された瞬間に決まる
this にまつわる混乱のほとんどは、「this は関数が 定義された 場所で決まる」という思い込みから生まれます。実はそうではありません。this の値は、関数が どのように呼ばれたか で決まります。
同じ関数でも、呼び方を変えるとどうなるか見てみましょう。
同じ関数でも、this の中身はこうも変わります。最初の呼び出しではドットの前に user があるので、this は user になります。一方、2 回目の呼び出しではドットの前に何もないため、this は undefined です。関数自体は一切変えていません。変わったのは「どう呼び出したか」だけです。
ここで覚えておいてほしいポイントは 1 つ。this の正体を知りたければ、関数の定義ではなく呼び出し方を見るということです。
this バインディングの 4 つのルール
this が決まるパターンは全部で 4 つあります。this まわりでつまずいたときは、だいたいこの 4 つのうちどれに当てはまるかを考えれば答えが出ます。
1. メソッド呼び出し: obj.fn()
関数がオブジェクトのプロパティとして呼び出された場合、this はそのオブジェクトになります。
ドットの前にあるものが、そのまま this になります。シンプルですね。
2. 普通の関数呼び出し: fn()
前に何もオブジェクトを付けずに呼び出した場合、strictモードでは this は undefined になります(モジュールやクラスの中では自動的にstrictモードが有効になります)。逆にsloppyモードではグローバルオブジェクトが入ります。
ここで出てくるのが、あの悪名高い「this is undefined」エラーです。オブジェクトからメソッドを取り出した瞬間、メソッド呼び出しはただの関数呼び出しに変わってしまいます。
counter. を付けずに呼び出しているので、バインドもされません。関数は自分がどのオブジェクトから来たかを覚えていないのです。
3. 明示的なバインディング: .call()、.apply()、.bind()
this を好きな値に強制的に設定することもできます。
.call と .apply はその場で関数を呼び出す点は同じで、違いは引数の渡し方だけです。一方 .bind は this を固定した新しい関数を返すので、コールバックに渡したいときに重宝します。
4. new 呼び出し: new Fn()
関数を new 付きで呼び出すと、新しいオブジェクトが生成され、それが this に束縛されます。
クラスも内部的にこの仕組みを利用しています。詳しくは後の章で取り上げます。
アロー関数には自分の this がない
アロー関数は、ここまで説明してきたルールをあえて無視します。そもそも this をバインドしません。代わりに、アロー関数が定義された時点の外側のスコープの this をそのまま引き継ぎます。
モジュールのトップレベルで定義したアロー関数は、そのモジュールの this、つまり undefined をキャプチャします。user.arrow() のように呼び出しても、アロー関数は頑として this を再バインドしてくれません。
一見バグのように見えますが、これこそがアロー関数の本質です。アロー関数が本領を発揮するのは、外側の this をそのまま引き継ぎたいメソッドの内部です。
setInterval の中のアロー関数は、start から this を引き継ぎます。timer.start() という形で呼ばれているので、this.seconds もちゃんと動くわけです。これがもし普通の function だったら、setInterval が渡してくる独自の this になってしまって動きません。
覚えておきたいルール: メソッドの中のコールバックにはアロー関数、メソッド自体には通常の関数を使う。 これが基本です。
よくあるコールバックの落とし穴
this が壊れるパターンで一番よく見かけるのがこれ。メソッドをコールバックとして渡した瞬間に、バインディングが失われます。
setTimeout は c.increment() のようなメソッド呼び出しではなく、ただの関数呼び出しとして実行してしまいます。これを直す方法は次の3つです。
どの方法でも動きますが、基本的にはアロー関数でラップするのが一番わかりやすいですね。
トップレベルでの this
トップレベルの this が何を指すかは、そのコードがどこで実行されるかによって変わります。
- ブラウザのスクリプト(モジュールではない場合):
thisはwindowになります。 - ESモジュール(最近のバンドルされたコードのほとんどが該当):
thisはundefinedです。 - Node.js の CommonJS モジュール:
thisはmodule.exportsを指します。
環境を問わずグローバルオブジェクトを確実に参照したい場合は、globalThis を使いましょう。
実務ではトップレベルの this に頼らない方が無難です。グローバルオブジェクトが本当に必要な場面では globalThis を使い、それ以外は値を明示的に渡していきましょう。
this の挙動を判定するフロー
this が何を指すのか分からなくなったら、次の順番で上から確認していけば答えが出ます。
- その関数はアロー関数か? そうなら
thisは外側のスコープのものをそのまま引き継ぎます。呼び出し側がどうであっても関係ありません。 new付きで呼ばれたか? そうならthisは新しく作られたオブジェクトです。.call・.apply・bind された関数として呼ばれたか? そうなら渡された値がthisになります。obj.method()の形で呼ばれたか? そうならthisはobjです。- ただの
fn()として呼ばれたか? strictモードではthisはundefinedになります。
この順に上から当てはめていけば、どんなケースでも決着がつきます。
次は高階関数へ
this の正体がはっきりしたところで、いよいよ JavaScript らしさの本丸、関数を値として持ち回すパターンに進みましょう。次回は高階関数 (higher-order functions) ――つまり関数を引数に取ったり関数を返したりする関数――を取り上げます。配列メソッドやイベントハンドラ、そして実際の JavaScript コードの大半を支えているのが、まさにこの仕組みです。
よくある質問
JavaScriptのthisは何を指しているの?
thisは何を指しているの?thisが指すのは「その関数がどのオブジェクトから呼ばれたか」であって、どこで定義されたかではありません。たとえばuser.greet()と呼べばthisはuserになります。一方、ただのgreet()として呼ぶと、strictモードではundefined、sloppyモードではグローバルオブジェクトになります。ポイントは定義場所ではなく、**呼び出し場所(call site)**だということです。
関数の中でthisがundefinedになるのはなぜ?
thisがundefinedになるのはなぜ?よくある原因は、メソッドをオブジェクトから外して単独で呼び出してしまうパターン、もしくはコールバックとして渡してしまうパターンです。const fn = user.greet; fn();のように書くと、呼び出し時にドットの左側にオブジェクトが無くなるので、バインディングが失われてしまいます。対処法は.bind(user)を使う、アロー関数でラップする、あるいは素直にuser.greet()として呼ぶ、のいずれかです。
アロー関数のthisは何が違うの?
thisは何が違うの?アロー関数は自分自身のthisを持ちません。定義された時点の外側のスコープのthisをそのまま引き継ぎます。そのため、メソッド内部のコールバックで「外側のthisをそのまま使いたい」ときにとても便利です。逆に言うと、.call()や.apply()、.bind()を使ってもアロー関数のthisを上書きすることはできません。
スクリプトのトップレベルでのthisはどうなるの?
thisはどうなるの?ブラウザの通常スクリプトでは、トップレベルのthisはwindowオブジェクトです。ESモジュール内ではundefinedになり、Node.jsのCommonJSモジュールではmodule.exportsを指します。環境に依存せず必ずグローバルオブジェクトを参照したい場合は、globalThisを使うのが確実です。