はじめに

JavaScriptを学ぶ上で、多くの初心者がつまずくポイントの一つがthisキーワードです。thisは「呼び出し方」によって参照先が変わるという特性を持っており、他のプログラミング言語と異なる挙動を示すことがあります。

本記事では、JavaScriptのthisについて以下の内容を初心者向けにわかりやすく解説します。

  • thisとは何か、なぜ重要なのか
  • グローバルコンテキストでのthis
  • 通常の関数でのthis
  • オブジェクトのメソッドでのthis
  • イベントハンドラーでのthis
  • クラスでのthis
  • アロー関数でのthis
  • callapplybindによるthisの明示的な指定

thisの挙動を正しく理解することで、意図しないバグを防ぎ、より堅牢なJavaScriptコードを書けるようになります。

thisとは

thisの基本概念

thisは、JavaScriptの特別なキーワードで、現在のコードが実行されているコンテキスト(文脈)を参照します。簡単に言えば、「今、誰がこのコードを実行しているか」を指し示すものです。

1
2
3
4
5
6
7
8
const person = {
  name: "田中",
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
};

person.greet(); // こんにちは、田中です

上記の例では、greetメソッド内のthispersonオブジェクトを参照しています。this.nameperson.nameと同じ意味になり、「田中」が出力されます。

thisが重要な理由

thisを正しく理解することで、以下のメリットがあります。

  • 同じメソッドを異なるオブジェクトで再利用できる
  • オブジェクト指向プログラミングの基盤を理解できる
  • コールバック関数やイベントハンドラーでのバグを防げる
  • 他のJavaScriptライブラリやフレームワークのコードを読み解ける

thisの値が決まるタイミング

JavaScriptのthisの値は、関数が定義されたときではなく、呼び出されたときに決まります。これがJavaScriptのthisを理解する上で最も重要なポイントです。

1
2
3
4
5
6
7
8
9
function showThis() {
  console.log(this);
}

const obj1 = { name: "obj1", showThis };
const obj2 = { name: "obj2", showThis };

obj1.showThis(); // { name: 'obj1', showThis: [Function: showThis] }
obj2.showThis(); // { name: 'obj2', showThis: [Function: showThis] }

同じ関数showThisでも、呼び出し方によってthisの値が異なることがわかります。

コンテキスト別のthisの挙動

thisの値は、コードが実行されるコンテキストによって異なります。ここでは、各コンテキストでのthisの挙動を詳しく見ていきます。

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

グローバルスコープ(関数やクラスの外)でのthisは、グローバルオブジェクトを参照します。

1
2
3
4
5
// ブラウザ環境の場合
console.log(this === window); // true

// Node.js環境の場合(モジュール外)
console.log(this === globalThis); // true

ブラウザではwindowオブジェクト、Node.jsではglobalオブジェクトがグローバルオブジェクトとなります。ES2020以降では、環境に依存しないglobalThisを使用することが推奨されています。

注意点: ESモジュールとして読み込まれたスクリプト(<script type="module">)では、トップレベルのthisundefinedになります。

1
2
3
<script type="module">
  console.log(this); // undefined
</script>

通常の関数でのthis

通常の関数内でのthisは、関数の呼び出し方によって決まります。

単独で呼び出した場合

関数をオブジェクトに関連付けずに直接呼び出した場合、thisの値は実行モードによって異なります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function showThis() {
  console.log(this);
}

// 非strictモード
showThis(); // Window オブジェクト(ブラウザの場合)

// strictモード
function showThisStrict() {
  "use strict";
  console.log(this);
}
showThisStrict(); // undefined

非strictモードではthisがグローバルオブジェクトに置き換えられますが、strictモードではundefinedのままです。これは意図しないグローバル変数の作成を防ぐためのJavaScriptの安全機能です。

メソッドとして呼び出した場合

関数がオブジェクトのメソッドとして呼び出された場合、thisはそのオブジェクトを参照します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const calculator = {
  value: 10,
  add(num) {
    return this.value + num;
  },
  multiply(num) {
    return this.value * num;
  }
};

console.log(calculator.add(5));      // 15
console.log(calculator.multiply(3)); // 30

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

オブジェクトのメソッド内でのthisは、そのメソッドを呼び出したオブジェクトを参照します。これは最も一般的なthisの使い方です。

1
2
3
4
5
6
7
8
9
const user = {
  firstName: "太郎",
  lastName: "山田",
  getFullName() {
    return `${this.lastName} ${this.firstName}`;
  }
};

console.log(user.getFullName()); // 山田 太郎

メソッドを別の変数に代入した場合の注意点

メソッドを変数に代入して呼び出すと、thisの参照先が変わってしまいます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const user = {
  name: "佐藤",
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
};

user.greet(); // こんにちは、佐藤です

const greetFunc = user.greet;
greetFunc(); // こんにちは、undefinedです(strictモードではエラー)

greetFunc()はオブジェクトに関連付けられずに呼び出されるため、thisがグローバルオブジェクト(またはundefined)を参照してしまいます。

ネストしたオブジェクトでのthis

thisは、メソッドを直接呼び出したオブジェクトを参照します。プロトタイプチェーン上のオブジェクトではなく、実際に呼び出しに使用されたオブジェクトです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const parent = {
  name: "親オブジェクト",
  showName() {
    console.log(this.name);
  }
};

const child = {
  name: "子オブジェクト",
  __proto__: parent
};

child.showName(); // 子オブジェクト

イベントハンドラーでのthis

DOMイベントハンドラーとして使用される関数内でのthisは、イベントが発生した要素を参照します。

1
2
3
4
5
6
const button = document.getElementById("myButton");

button.addEventListener("click", function(event) {
  console.log(this === event.currentTarget); // true
  this.style.backgroundColor = "blue";
});

上記の例では、thisはクリックされたbutton要素を参照します。これにより、イベントが発生した要素に対して直接操作を行うことができます。

インラインイベントハンドラーでのthis

HTMLのイベント属性で定義されたハンドラーでも、thisはその要素を参照します。

1
2
<button onclick="console.log(this.tagName)">クリック</button>
<!-- 出力: BUTTON -->

ただし、インラインハンドラー内で定義された関数は、通常の関数呼び出しと同じルールに従います。

1
2
<button onclick="(function() { console.log(this); })()">クリック</button>
<!-- 出力: Window オブジェクト(非strictモード) -->

クラスでのthis

ES6で導入されたクラス構文では、thisはクラスのインスタンスを参照します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

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

  celebrateBirthday() {
    this.age++;
    console.log(`お誕生日おめでとう!${this.age}歳になりました`);
  }
}

const person = new Person("花子", 25);
person.introduce();         // 私は花子、25歳です
person.celebrateBirthday(); // お誕生日おめでとう!26歳になりました

クラスメソッドをコールバックとして渡す場合の問題

クラスメソッドを他の関数にコールバックとして渡すと、thisの参照が失われることがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Counter {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
    console.log(this.count);
  }
}

const counter = new Counter();
counter.increment(); // 1

// コールバックとして渡すと問題が発生
setTimeout(counter.increment, 1000); // NaN(thisがundefined)

この問題を解決する方法については、後述の「bindによるthisの固定」で説明します。

静的メソッドでのthis

静的メソッド内でのthisは、クラス自体を参照します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MathUtils {
  static PI = 3.14159;

  static circleArea(radius) {
    return this.PI * radius * radius;
  }

  static info() {
    console.log(`これは${this.name}クラスです`);
  }
}

console.log(MathUtils.circleArea(5)); // 78.53975
MathUtils.info(); // これはMathUtilsクラスです

アロー関数でのthis

アロー関数は、通常の関数とは異なり、独自のthisを持ちません。代わりに、定義された時点での外側のスコープのthisを継承(レキシカルスコープ)します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const obj = {
  name: "オブジェクト",
  
  // 通常の関数: thisは呼び出し元によって決まる
  regularFunc: function() {
    console.log("通常の関数:", this.name);
  },
  
  // アロー関数: thisは定義時の外側スコープを継承
  arrowFunc: () => {
    console.log("アロー関数:", this.name);
  }
};

obj.regularFunc(); // 通常の関数: オブジェクト
obj.arrowFunc();   // アロー関数: undefined(グローバルスコープのthis)

アロー関数のthisは、objではなく、アロー関数が定義された時点の外側スコープ(この場合はグローバルスコープ)のthisを参照します。

アロー関数が有効なケース

アロー関数のthisの特性は、コールバック関数で特に有効です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // アロー関数を使うと、thisがTimerインスタンスを参照し続ける
    setInterval(() => {
      this.seconds++;
      console.log(`${this.seconds}秒経過`);
    }, 1000);
  }
}

const timer = new Timer();
timer.start();
// 1秒経過
// 2秒経過
// 3秒経過...

通常の関数を使った場合、setIntervalのコールバック内でのthisはグローバルオブジェクトを参照してしまいます。アロー関数を使うことで、外側のthis(Timerインスタンス)を保持できます。

アロー関数を使うべきでないケース

以下のケースでは、アロー関数の使用は避けるべきです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// オブジェクトのメソッドとして使う場合
const counter = {
  count: 0,
  // 良くない: thisがcounterを参照しない
  increment: () => {
    this.count++;
  }
};

counter.increment();
console.log(counter.count); // 0(期待通りに動作しない)
1
2
3
4
5
6
// コンストラクター関数として使う場合
const Person = (name) => {
  this.name = name;
};

// const p = new Person("太郎"); // TypeError: Person is not a constructor

thisの明示的な指定

JavaScriptでは、callapplybindメソッドを使ってthisの値を明示的に指定できます。

callメソッド

callメソッドは、関数を呼び出す際にthisの値を指定できます。引数は個別に渡します。

1
2
3
4
5
6
7
8
9
function greet(greeting, punctuation) {
  console.log(`${greeting}${this.name}${punctuation}`);
}

const person1 = { name: "山田" };
const person2 = { name: "鈴木" };

greet.call(person1, "こんにちは", "!");  // こんにちは、山田!
greet.call(person2, "おはよう", "。");    // おはよう、鈴木。

applyメソッド

applyメソッドはcallと同様ですが、引数を配列として渡します。

1
2
3
4
5
6
7
8
function introduce(hobby1, hobby2) {
  console.log(`私は${this.name}です。趣味は${hobby1}${hobby2}です。`);
}

const user = { name: "佐藤" };
const hobbies = ["読書", "映画鑑賞"];

introduce.apply(user, hobbies); // 私は佐藤です。趣味は読書と映画鑑賞です。

callapplyの使い分けは、引数の渡し方の違いだけです。配列で引数を持っている場合はapplyが便利です。

bindメソッド

bindメソッドは、thisの値を永続的に固定した新しい関数を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const module = {
  x: 42,
  getX() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined(thisがmoduleではない)

const boundGetX = module.getX.bind(module);
console.log(boundGetX()); // 42

クラスメソッドのバインド

先ほど紹介したクラスメソッドをコールバックとして渡す問題は、bindで解決できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Counter {
  constructor() {
    this.count = 0;
    // コンストラクターでメソッドをバインド
    this.increment = this.increment.bind(this);
  }

  increment() {
    this.count++;
    console.log(this.count);
  }
}

const counter = new Counter();
setTimeout(counter.increment, 1000); // 1(正しく動作する)

または、クラスフィールドとアロー関数を組み合わせる方法もあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Counter {
  count = 0;

  // アロー関数で定義するとthisが自動的にバインドされる
  increment = () => {
    this.count++;
    console.log(this.count);
  };
}

const counter = new Counter();
setTimeout(counter.increment, 1000); // 1

thisの動作パターンまとめ

ここまで学んだthisの動作パターンを表にまとめます。

コンテキスト thisの値 備考
グローバル(非strictモード) グローバルオブジェクト ブラウザではwindow
グローバル(strictモード) グローバルオブジェクト モジュールではundefined
通常の関数(単独呼び出し・非strict) グローバルオブジェクト this置換が発生
通常の関数(単独呼び出し・strict) undefined 安全な動作
オブジェクトのメソッド 呼び出し元のオブジェクト ドットの左側のオブジェクト
イベントハンドラー イベントが発生した要素 event.currentTargetと同じ
コンストラクター 新しく作成されるインスタンス newキーワード使用時
クラスメソッド クラスのインスタンス 静的メソッドはクラス自体
アロー関数 定義時の外側スコープのthis 呼び出し方に関わらず固定
call/apply/bind 第1引数で指定した値 明示的な指定

thisの判定フローチャート

thisの値を判定する際は、以下のフローチャートが参考になります。

flowchart TD
    A[関数が呼び出された] --> B{アロー関数か?}
    B -->|はい| C[定義時の外側スコープのthisを使用]
    B -->|いいえ| D{new演算子で呼び出されたか?}
    D -->|はい| E[新しいオブジェクトがthis]
    D -->|いいえ| F{call/apply/bindが使われたか?}
    F -->|はい| G[指定されたオブジェクトがthis]
    F -->|いいえ| H{メソッドとして呼び出されたか?}
    H -->|はい| I[ドットの左側のオブジェクトがthis]
    H -->|いいえ| J{strictモードか?}
    J -->|はい| K[thisはundefined]
    J -->|いいえ| L[thisはグローバルオブジェクト]

よくある間違いと対処法

コールバック関数でのthisの喪失

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const obj = {
  name: "テスト",
  delayedLog() {
    setTimeout(function() {
      console.log(this.name); // undefined
    }, 1000);
  }
};

obj.delayedLog();

対処法1: アロー関数を使用

1
2
3
4
5
6
7
8
const obj = {
  name: "テスト",
  delayedLog() {
    setTimeout(() => {
      console.log(this.name); // テスト
    }, 1000);
  }
};

対処法2: bindを使用

1
2
3
4
5
6
7
8
const obj = {
  name: "テスト",
  delayedLog() {
    setTimeout(function() {
      console.log(this.name); // テスト
    }.bind(this), 1000);
  }
};

対処法3: thisを変数に保存

1
2
3
4
5
6
7
8
9
const obj = {
  name: "テスト",
  delayedLog() {
    const self = this; // thisを変数に保存
    setTimeout(function() {
      console.log(self.name); // テスト
    }, 1000);
  }
};

配列メソッドのコールバックでのthis

配列のforEachmapfilterなどのメソッドでは、第2引数でthisを指定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const processor = {
  prefix: "[処理済] ",
  process(items) {
    return items.map(function(item) {
      return this.prefix + item;
    }, this); // 第2引数でthisを指定
  }
};

console.log(processor.process(["A", "B", "C"]));
// ["[処理済] A", "[処理済] B", "[処理済] C"]

アロー関数を使えば、第2引数は不要です。

1
2
3
4
5
6
const processor = {
  prefix: "[処理済] ",
  process(items) {
    return items.map(item => this.prefix + item);
  }
};

まとめ

本記事では、JavaScriptのthisキーワードについて詳しく解説しました。

  • thisの値は関数が呼び出されたときに決まる
  • グローバルコンテキストではthisはグローバルオブジェクトを参照
  • メソッドとして呼び出された場合、thisは呼び出し元のオブジェクトを参照
  • アロー関数は独自のthisを持たず、定義時の外側スコープを継承
  • callapplybindthisを明示的に指定可能

thisの挙動は最初は複雑に感じるかもしれませんが、基本的なルールを理解すれば予測可能です。特にアロー関数と通常の関数の違いを意識することで、多くのバグを防ぐことができます。

次のステップとして、Promiseやasync/awaitによる非同期処理について学ぶと、コールバック関数でのthisの扱いがより明確になります。

参考リンク