T-CREATOR

JavaScript の this キーワードを完全理解!初心者がつまずくポイント解説

JavaScript の this キーワードを完全理解!初心者がつまずくポイント解説

JavaScript を学習していると、必ず出会うのが this キーワードです。しかし、他のプログラミング言語とは異なる動作をするため、多くの初心者の方が混乱してしまいます。

本記事では、JavaScript の this について、つまずきやすいポイントを含めて丁寧に解説いたします。最後まで読んでいただければ、this の動作が完全に理解でき、自信を持ってコードを書けるようになるでしょう。

背景

JavaScript の this は特殊な存在

JavaScript の this は、他のプログラミング言語(Java や C# など)とは大きく異なる動作をします。多くの言語では this は「現在のオブジェクトインスタンス」を指すのが一般的ですが、JavaScript では 実行時のコンテキスト によって値が決まるのです。

この特殊な仕様は JavaScript の柔軟性を生み出している一方で、初心者の方には理解の壁となってしまいます。実際、多くのエンジニアが JavaScript 学習の際に最も苦労する概念の一つとして this を挙げているんですね。

以下の図で、JavaScript の this の特殊性を他言語と比較してみましょう。

mermaidflowchart TD
  other[他の言語] -->|固定的| obj[オブジェクトインスタンス]
  js[JavaScript] -->|動的| context[実行コンテキスト]
  context --> globalObj[グローバルオブジェクト]
  context --> methodOwner[メソッドの所有者]
  context --> constructorFn[新しいインスタンス]
  context --> explicitBind[明示的バインド]

図からわかるように、JavaScript では実行時の状況によって this が指す対象が変わります。この動的な性質こそが、理解を困難にしている要因なのです。

実行コンテキストが決める this の値

JavaScript エンジンは、関数が呼び出される際に 実行コンテキスト を作成します。この実行コンテキストの中で this の値が決定されるため、同じ関数でも呼び出し方によって this の値が変わってしまうのです。

javascriptconst person = {
  name: 'Taro',
  greet: function () {
    console.log(`Hello, ${this.name}`);
  },
};

// 同じ関数でも呼び出し方で this が変わる
person.greet(); // "Hello, Taro"
const greetFunc = person.greet;
greetFunc(); // "Hello, undefined" または エラー

この例では、同じ greet 関数が異なる結果を返しています。これが JavaScript の this の特殊性を表す典型的な例なんですね。

課題

this の値が呼び出し方で変わる複雑さ

初心者の方が最も混乱するのは、同じ関数でも呼び出し方によって this の値が変わる ことです。オブジェクト指向プログラミングに慣れている方ほど、この動作に戸惑ってしまいます。

javascriptconst calculator = {
  value: 0,
  add: function (num) {
    this.value += num;
    return this.value;
  },
};

// メソッドとして呼び出し(期待通り動作)
console.log(calculator.add(5)); // 5

// 関数として取り出して呼び出し(期待と異なる動作)
const addFunc = calculator.add;
addFunc(3); // エラーまたは予期しない結果

このような予期しない動作により、「なぜ動かないの?」という疑問が生まれ、JavaScript 学習の大きな壁となってしまいます。

アロー関数と通常関数での this の違い

ES6 で導入されたアロー関数は、this の扱いが通常の関数と全く異なります。この違いを理解せずに使い分けることで、さらなる混乱が生じてしまうのです。

以下の図で、通常関数とアロー関数での this の決定方法を比較してみましょう。

mermaidflowchart LR
  call[関数呼び出し] --> normal{通常関数?}
  normal -->|Yes| dynamic[動的に this を決定]
  normal -->|No| arrow[アロー関数]
  arrow --> lexical[レキシカルスコープの this を使用]
  dynamic --> caller[呼び出し元によって決定]
  lexical --> parent[定義時の親スコープの this]

アロー関数は定義時点で this が固定されるため、呼び出し方に関係なく常に同じ this を参照します。この特性を理解しないまま使うと、期待した動作にならないことがあります。

メソッド、コールバック、イベントハンドラでの予期しない動作

実際の開発では、メソッドをコールバック関数として渡したり、イベントハンドラとして設定したりする場面が多くあります。このような場合に this の値が予期しない値になることが頻発します。

javascriptclass Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // setInterval のコールバック内での this は Timer インスタンスを指さない
    setInterval(function () {
      this.seconds++; // this は window または undefined
      console.log(this.seconds); // NaN または エラー
    }, 1000);
  }
}

このようなコードは一見正しく見えますが、実際には期待通りに動作しません。コールバック関数内の this が Timer インスタンスを指していないからです。

解決策

this の 4 つのバインディングルールの理解

JavaScript の this は、以下の 4 つのルールに従って値が決まります。この優先順位を理解することで、this の値を正確に予測できるようになります。

#バインディングルール説明優先度
1new バインディングnew 演算子で関数を呼び出した場合最高
2明示的バインディングcall, apply, bind を使った場合
3暗黙的バインディングオブジェクトのメソッドとして呼び出した場合
4デフォルトバインディング上記以外の場合最低

この優先順位を覚えることで、どのような状況でも this の値を正確に判断できるようになります。

以下の図で、バインディングルールの適用プロセスを可視化してみましょう。

mermaidflowchart TD
  start[関数呼び出し] --> new_check{new で呼び出し?}
  new_check -->|Yes| new_binding[new バインディング<br/>新しいインスタンス]
  new_check -->|No| explicit_check{call/apply/bind?}
  explicit_check -->|Yes| explicit_binding[明示的バインディング<br/>指定されたオブジェクト]
  explicit_check -->|No| implicit_check{オブジェクトメソッド?}
  implicit_check -->|Yes| implicit_binding[暗黙的バインディング<br/>呼び出し元オブジェクト]
  implicit_check -->|No| default_binding[デフォルトバインディング<br/>グローバル or undefined]

このフローチャートに従って判断することで、this の値を確実に特定できます。

バインドメソッドの活用法

this の値を明示的にコントロールするために、JavaScript では call(), apply(), bind() の 3 つのメソッドが用意されています。

call() メソッド

call() メソッドは、this の値を指定して関数を即座に実行します。

javascriptfunction introduce() {
  console.log(`私は${this.name}です。${this.age}歳です。`);
}

const person1 = { name: '田中', age: 25 };
const person2 = { name: '佐藤', age: 30 };

// this を person1 に設定して実行
introduce.call(person1); // "私は田中です。25歳です。"
introduce.call(person2); // "私は佐藤です。30歳です。"

apply() メソッド

apply()call() と似ていますが、引数を配列として渡す点が異なります。

javascriptfunction calculateSum(a, b, c) {
  console.log(`${this.name}の計算結果: ${a + b + c}`);
  return a + b + c;
}

const calculator = { name: '計算機' };

// 引数を個別に渡す場合
calculateSum.call(calculator, 1, 2, 3); // "計算機の計算結果: 6"

// 引数を配列で渡す場合
calculateSum.apply(calculator, [1, 2, 3]); // "計算機の計算結果: 6"

bind() メソッド

bind() は新しい関数を作成し、その関数の this を永続的に固定します。

javascriptclass EventHandler {
  constructor() {
    this.message = 'ボタンがクリックされました!';
  }

  handleClick() {
    console.log(this.message);
  }
}

const handler = new EventHandler();

// bind() で this を固定した新しい関数を作成
const boundHandler = handler.handleClick.bind(handler);

// コールバック関数として安全に使用可能
document
  .getElementById('btn')
  .addEventListener('click', boundHandler);

適切な関数定義の選択方法

状況に応じて通常関数とアロー関数を使い分けることで、this に関する問題を予防できます。

通常関数を選ぶべき場面

  • オブジェクトのメソッドを定義する場合
  • コンストラクタ関数を作成する場合
  • this の値を動的に変更したい場合
javascriptconst userService = {
  users: ['Alice', 'Bob', 'Charlie'],

  // オブジェクトメソッドは通常関数で定義
  getUsers: function () {
    return this.users; // userService を指す
  },

  addUser: function (name) {
    this.users.push(name);
    return this.users.length;
  },
};

アロー関数を選ぶべき場面

  • コールバック関数として使用する場合
  • 親スコープの this を保持したい場合
  • イベントハンドラで外側のコンテキストを参照したい場合
javascriptclass Counter {
  constructor() {
    this.count = 0;
  }

  startTimer() {
    // アロー関数で親スコープ(Counter インスタンス)の this を保持
    setInterval(() => {
      this.count++; // Counter インスタンスを指す
      console.log(this.count);
    }, 1000);
  }
}

具体例

グローバルコンテキストでの this

グローバルスコープ(どの関数にも属さない場所)で this を参照すると、ブラウザ環境では window オブジェクト、Node.js 環境では global オブジェクトを指します。

javascript// ブラウザ環境
console.log(this); // window オブジェクト
console.log(this === window); // true

// strict mode では undefined
('use strict');
console.log(this); // undefined

グローバルコンテキストでの this は実際の開発ではほとんど使用しませんが、理解しておくことで他の場面での動作予測に役立ちます。

オブジェクトメソッドでの this

オブジェクトのプロパティとして定義された関数(メソッド)では、this はそのオブジェクト自身を指します。これが最も直感的で理解しやすいパターンです。

javascriptconst book = {
  title: 'JavaScript 入門',
  author: '山田太郎',
  pages: 320,

  // メソッド内の this は book オブジェクトを指す
  getInfo: function () {
    return `『${this.title}』著者:${this.author}${this.pages}ページ`;
  },
};

console.log(book.getInfo());
// "『JavaScript 入門』著者:山田太郎、320ページ"

ただし、メソッドを変数に代入してから呼び出すと、this の値が変わってしまいます。

javascript// メソッドを変数に代入
const getBookInfo = book.getInfo;

// この時点で this は book オブジェクトではなくなる
console.log(getBookInfo()); // エラーまたは予期しない結果

このような問題を回避するために、次に説明するバインドメソッドを使用します。

コンストラクタ関数での this

new 演算子を使って関数を呼び出すと、その関数内の this は新しく作成されるオブジェクトを指します。これがコンストラクタ関数の仕組みです。

javascript// コンストラクタ関数の定義
function Car(make, model, year) {
  // this は新しく作成される Car インスタンス
  this.make = make;
  this.model = model;
  this.year = year;

  this.getInfo = function () {
    return `${this.year}年式 ${this.make} ${this.model}`;
  };
}

// new 演算子で新しいインスタンスを作成
const myCar = new Car('Toyota', 'Prius', 2022);
console.log(myCar.getInfo()); // "2022年式 Toyota Prius"

コンストラクタ関数内では、this を通じて新しいオブジェクトにプロパティやメソッドを追加できます。

以下の図で、コンストラクタ関数での this のバインディング過程を見てみましょう。

mermaidsequenceDiagram
  participant caller as 呼び出し元
  participant engine as JS エンジン
  participant instance as 新インスタンス

  caller->>engine: new Car('Toyota', 'Prius')
  engine->>instance: 空のオブジェクト作成
  engine->>engine: this = 新インスタンス
  engine->>instance: this.make = 'Toyota'
  engine->>instance: this.model = 'Prius'
  engine->>caller: インスタンスを返却

この過程により、new 演算子使用時は必ず新しいインスタンスが this となります。

アロー関数での this

アロー関数は、定義された時点で親スコープの this を「キャプチャ」します。その後、どのように呼び出されても this の値は変わりません。

javascriptconst team = {
  name: '開発チーム',
  members: ['Alice', 'Bob', 'Charlie'],

  // 通常関数でのメソッド
  showMembers: function () {
    console.log(`${this.name}のメンバー:`);

    // アロー関数は親スコープ(showMembers)の this を継承
    this.members.forEach((member) => {
      console.log(`- ${member} (チーム: ${this.name})`);
    });
  },
};

team.showMembers();
// 開発チームのメンバー:
// - Alice (チーム: 開発チーム)
// - Bob (チーム: 開発チーム)
// - Charlie (チーム: 開発チーム)

もし forEach の中で通常関数を使用した場合は、以下のような問題が発生します。

javascriptconst team = {
  name: '開発チーム',
  members: ['Alice', 'Bob'],

  showMembers: function () {
    this.members.forEach(function (member) {
      // この this は team オブジェクトを指さない
      console.log(`- ${member} (チーム: ${this.name})`); // エラー
    });
  },
};

イベントハンドラでの this

DOM イベントハンドラでは、this は通常イベントが発生した要素を指します。しかし、クラスのメソッドをイベントハンドラとして使用する場合は注意が必要です。

javascriptclass ButtonController {
  constructor(element) {
    this.element = element;
    this.clickCount = 0;
  }

  // 通常関数でのイベントハンドラ
  handleClick() {
    this.clickCount++; // この this は button 要素を指す
    console.log(`クリック数: ${this.clickCount}`); // エラー
  }

  // 正しい実装方法 1: bind() を使用
  setupWithBind() {
    this.element.addEventListener(
      'click',
      this.handleClick.bind(this)
    );
  }

  // 正しい実装方法 2: アロー関数を使用
  handleClickArrow = () => {
    this.clickCount++; // この this は ButtonController インスタンス
    console.log(`クリック数: ${this.clickCount}`);
  };
}

以下の図で、イベントハンドラでの this のバインディングパターンを整理してみましょう。

mermaidflowchart TD
  event[イベント発生] --> handler_type{ハンドラの種類}
  handler_type -->|通常関数| element[this = イベント要素]
  handler_type -->|アロー関数| lexical[this = 定義時の親スコープ]
  handler_type -->|bind済み関数| bound[this = bind で指定したオブジェクト]

  element --> problem[期待と異なる動作]
  lexical --> expected[期待通りの動作]
  bound --> expected

この図からわかるように、通常関数をそのまま使うと期待と異なる結果になりがちです。

まとめ

this の動作パターン総括

JavaScript の this について、重要なポイントを振り返りましょう。

図で理解できる要点

  • this は実行時のコンテキストで動的に決まる
  • 4 つのバインディングルールには優先順位がある
  • アロー関数は定義時の this を固定する
呼び出しパターンthis の値使用場面
obj.method()obj オブジェクトオブジェクトメソッド
new Constructor()新しいインスタンスオブジェクト生成
func.call(obj)指定したオブジェクト明示的制御
func()window/global または undefined通常の関数呼び出し
アロー関数定義時の親スコープコールバック関数

適切な使い分けのガイドライン

this を正しく使うための実践的なガイドラインです。

通常関数を使う場面

  • オブジェクトのメソッドを定義するとき
  • コンストラクタ関数を作成するとき
  • this の値を動的に変更したいとき

アロー関数を使う場面

  • コールバック関数として使用するとき
  • 親スコープの this を保持したいとき
  • イベントハンドラで外側のコンテキストを参照したいとき

バインドメソッドを使う場面

  • メソッドを他の場所で使い回したいとき
  • this の値を明示的に固定したいとき
  • イベントハンドラで特定のオブジェクトを参照したいとき

これらのガイドラインを参考に、状況に応じて最適な方法を選択してください。最初は混乱するかもしれませんが、実際にコードを書きながら経験を積むことで、自然と適切な判断ができるようになります。

JavaScript の this は確かに複雑ですが、その仕組みを理解すれば、より柔軟で表現力豊かなコードが書けるようになるでしょう。ぜひ今日学んだ内容を実際のプロジェクトで活用してみてくださいね。

関連リンク