こんにちは、かつコーチです。
前回はクロージャについて解説しました。
今回のテーマは、JavaScriptを学んでいる人の多くがつまずく「this」です。
「thisって結局何を指してるの?」「さっきまで動いてたのに急にundefinedになった」——そんな経験がある人は多いと思います。
thisは「その関数がどう呼び出されたか」によって中身が変わる特殊なキーワードです。
他の言語のように「クラスのインスタンス自身」と決め打ちで覚えてしまうと、JavaScriptでは痛い目にあいます。
今日は、thisが「いつ」「何を指すのか」を、呼び出しパターンごとに整理していきます。
thisとは?
「呼び出し方」で中身が決まる特殊な変数
thisは、関数が実行されるときに自動的に渡される「呼び出し元の情報」を持つキーワードです。
重要なのは、thisの値は関数を「定義した場所」ではなく、「呼び出した場所・方法」で決まるという点です。
function showThis() {
console.log(this);
}
showThis(); // 呼び出し方によってthisの中身が変わる
同じshowThisという関数でも、呼び出し方が変わればthisの中身も変わります。
これがJavaScript特有の分かりにくさの正体です。
なぜこの仕組みが必要なのか
もしthisが常に固定だったら、1つの関数を「オブジェクトAのメソッドとして」使うことも「オブジェクトBのメソッドとして」使い回すこともできなくなります。
thisが呼び出し方に応じて変わる仕組みのおかげで、同じ処理を複数のオブジェクトで再利用できるのです。
とはいえ、この柔軟さが初中級者を悩ませる最大の要因でもあります。
私自身、実務で「なぜこのメソッド内のthisがundefinedになるんだろう」と1時間近く原因を探した経験があります。
原因は後述する「コールバックに渡すときにthisが外れる」パターンでした。
呼び出しパターン別のthis
パターン1:オブジェクトのメソッドとして呼ぶ
オブジェクトのプロパティとして関数を呼び出すと、thisはそのオブジェクト自身を指します。
const user = {
name: "かつコーチ",
greet() {
console.log(`こんにちは、${this.name}です`);
}
};
user.greet(); // こんにちは、かつコーチです
user.greet()という「.(ドット)の前にあるオブジェクト」がthisになる、と覚えておくと分かりやすいです。
パターン2:単独の関数として呼ぶ
オブジェクトを介さず関数だけを呼び出すと、thisはundefined(厳格モード)またはグローバルオブジェクトになります。
"use strict";
function showThis() {
console.log(this);
}
showThis(); // undefined(strictモードの場合)
現代のJavaScriptはモジュール(複数のファイルに分けたコードをimport・exportでやり取りする仕組み)を使うことが多く、モジュール内は自動的に厳格モードになるため、undefinedになるケースがほとんどです。
パターン3:コンストラクタとして呼ぶ
newをつけて関数(クラス)を呼び出すと、thisは新しく作られたインスタンスを指します。
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`こんにちは、${this.name}です`);
}
}
const katsu = new User("かつコーチ");
katsu.greet(); // こんにちは、かつコーチです
new User("かつコーチ")で作られたkatsuというインスタンスがthisの中身になります。
パターン4:アロー関数の場合
アロー関数(=>で書く関数)は少し特殊で、自分自身のthisを持ちません。
代わりに、定義された場所の外側(周りのスコープ)のthisをそのまま引き継ぎます。
const user = {
name: "かつコーチ",
greet: () => {
console.log(`こんにちは、${this.name}です`);
}
};
user.greet(); // こんにちは、undefinedです(thisはオブジェクトを指さない)
アロー関数のthisはオブジェクトのメソッド定義に使うと、意図せずundefinedになることが多いので注意が必要です。
つまずきやすいポイント:コールバックの中でthisが外れる
実際につまずいた場面
私が実務で最初にハマったのが、setTimeoutのコールバックの中でthisが使えなくなるパターンです。
❌ Before:通常の関数をコールバックに渡してthisが外れる
const timer = {
seconds: 0,
start() {
setTimeout(function () {
this.seconds++; // このthisはtimerを指していない
console.log(this.seconds); // NaN(this.secondsがundefinedになる)
}, 1000);
}
};
timer.start();
setTimeoutに渡したfunctionは、timer.start()とは別の呼び出し方(コールバックとしての単独呼び出し)をされるため、thisがtimerを指さなくなります。
私はこのとき、「メソッドの中に書いたんだから当然thisはtimerのはず」と思い込んでいて、console.log(this)を1行ずつ差し込んでようやく原因に気づきました。
✅ After:アロー関数を使ってthisを引き継ぐ
const timer = {
seconds: 0,
start() {
setTimeout(() => {
this.seconds++; // アロー関数なのでstartのthis(timer)を引き継ぐ
console.log(this.seconds); // 1
}, 1000);
}
};
timer.start();
アロー関数は自分のthisを持たないため、外側のstartメソッド内のthis(=timer)をそのまま使えます。
コールバック関数の中でthisを使いたいときは、まずアロー関数で書けないか検討する、というのが実務での基本方針になります。
bindメソッドで明示的にthisを固定する方法
アロー関数が使えない場面(すでにfunctionで書かれたコードを直す場合など)では、bindメソッドでthisを明示的に固定する方法もあります。
const timer = {
seconds: 0,
start() {
setTimeout(
function () {
this.seconds++;
console.log(this.seconds);
}.bind(this), // thisをtimerに固定してから渡す
1000
);
}
};
timer.start(); // 1
.bind(this)をつけることで、「この関数のthisは常にこの値にする」と固定した新しい関数を作れます。
アロー関数が登場する前は、このbindを使うのが定番のテクニックでした。
応用:thisを図解で整理する
4つのルールの優先順位
thisの値を判断するときは、次の優先順位で考えると迷いにくくなります。
newで呼ばれたか → インスタンス自身.bind()/.call()/.apply()で明示的に指定されているか → 指定した値- オブジェクトの
.(ドット)経由で呼ばれたか → そのオブジェクト - どれにも当てはまらないか →
undefined(厳格モード)またはグローバルオブジェクト
アロー関数だけはこのルールの対象外で、常に「定義された場所の外側のthis」を引き継ぐ点を忘れないようにしましょう。
クラスのメソッドをイベントリスナーに渡すときの注意
クラスのメソッドをそのままイベントリスナーに渡すと、コールバックと同じ理由でthisが外れます。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
// ❌ そのまま渡すとthisが外れる
// button.addEventListener("click", counter.increment);
// ✅ アロー関数でラップしてthisを固定する
button.addEventListener("click", () => counter.increment());
「メソッドを値として渡す瞬間、thisとのつながりが切れる」という感覚を持っておくと、この種のバグに気づきやすくなります。
まとめ
この記事のポイント
thisの中身は「関数の呼び出し方」で決まり、定義した場所では決まらない- オブジェクトのメソッドとして呼ぶと
thisはそのオブジェクトになる - コンストラクタ(
new)で呼ぶとthisは新しいインスタンスになる - アロー関数は自分の
thisを持たず、外側のスコープのthisを引き継ぐ - コールバックに関数を渡すときは
thisが外れやすいので、アロー関数やbindで対策する
次に読むべき記事
thisの挙動が理解できると、非同期処理でよく使うコールバック関数の中身もぐっと読みやすくなります。
→ 次の記事:コールバック関数とは?非同期処理の入り口