【JavaScript】thisの挙動を理解する

javascript-icon JavaScript

こんにちは、かつコーチです。

前回はクロージャについて解説しました。

今回のテーマは、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が呼び出し方に応じて変わる仕組みのおかげで、同じ処理を複数のオブジェクトで再利用できるのです。

とはいえ、この柔軟さが初中級者を悩ませる最大の要因でもあります。

私自身、実務で「なぜこのメソッド内のthisundefinedになるんだろう」と1時間近く原因を探した経験があります。

原因は後述する「コールバックに渡すときにthisが外れる」パターンでした。

呼び出しパターン別のthis

パターン1:オブジェクトのメソッドとして呼ぶ

オブジェクトのプロパティとして関数を呼び出すと、thisそのオブジェクト自身を指します。

const user = {
  name: "かつコーチ",
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
};

user.greet(); // こんにちは、かつコーチです

user.greet()という「.(ドット)の前にあるオブジェクト」がthisになる、と覚えておくと分かりやすいです。

パターン2:単独の関数として呼ぶ

オブジェクトを介さず関数だけを呼び出すと、thisundefined(厳格モード)またはグローバルオブジェクトになります。

"use strict";

function showThis() {
  console.log(this);
}

showThis(); // undefined(strictモードの場合)

現代のJavaScriptはモジュール(複数のファイルに分けたコードをimportexportでやり取りする仕組み)を使うことが多く、モジュール内は自動的に厳格モードになるため、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()とは別の呼び出し方(コールバックとしての単独呼び出し)をされるため、thistimerを指さなくなります。

私はこのとき、「メソッドの中に書いたんだから当然thistimerのはず」と思い込んでいて、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の値を判断するときは、次の優先順位で考えると迷いにくくなります。

  1. newで呼ばれたか → インスタンス自身
  2. .bind() / .call() / .apply()で明示的に指定されているか → 指定した値
  3. オブジェクトの.(ドット)経由で呼ばれたか → そのオブジェクト
  4. どれにも当てはまらないか → 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の挙動が理解できると、非同期処理でよく使うコールバック関数の中身もぐっと読みやすくなります。

→ 次の記事:コールバック関数とは?非同期処理の入り口

タイトルとURLをコピーしました