JavaScriptのthisを極める

jsコード JavaScript

Javaなど他のプログラミング言語にもthisは存在しますが、JavaScriptのthisは「どのように呼び出されるか」によって値が決まるため、完全に理解して使いこなすのが難しいと言われがちです。

そこで本記事では、JavaScriptのthisについて、ワンステップずつ理解を深められるよう説明を進めていきます。

一気にすべてを記憶・理解しようと焦らないでください。
この機会に、使用パターンごとの挙動を1つ1つ着実に理解し、サンプルを手を動かして実装しながら体得頂ければと思います。

thisの使用パターン一覧

まずは、パターンの全量を一覧化し、それぞれに簡単な説明を補足します。

  1. グローバルコンテキストの this
  2. 関数内の this
  3. メソッド内の this
  4. アロー関数内の this
  5. コンストラクタ関数内の this
  6. callapply、および bind メソッドによる this の明示的な設定
  7. ES6 のクラスで super を使用する場合の this

No.5までは、完全に理解頂きたい内容になっています。
No.6以降は、「thisの匠」レベルに極めたい方は頑張って理解してください。

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

this がグローバルスコープで直接使われるケースはそう多くありませんが、グローバルオブジェクトを理解するためにリストアップしています。

グローバルコンテキストのthis
console.log( this );

このように、グローバルスコープで使用された場合、thisは「グローバルオブジェクト」を指します。
ただし、strict modeで実行された場合には undefined を指すのでご注意ください。

  • strict mode
    • thisグローバルオブジェクトを指します。
  • strict mode
    • thisundefined になります。

グローバルオブジェクトは、JavaScriptがどのような環境で実行されるかによって変化します。

環境によるグローバルオブジェクトの変化

  • ウェブブラウザーで実行された場合
    • グローバルオブジェクトは Window です。ウェブにおける JavaScript コードのほとんどはこのケースに該当しますが、後述のウェブワーカー内では異なります。
  • ウェブワーカー API内で実行された場合:
    • グローバルオブジェクトは WorkerGlobalScope です。また、この場合、self キーワードも WorkerGlobalScope を指します。
  • Node.js で実行された場合:
    • グローバルオブジェクトは global と呼ばれるオブジェクトです。

2. 関数内の this

関数宣言(function宣言)で作成された関数内の this は、どのように呼び出されるかによって指し示すものが変化します。

単独での呼び出し

関数がオブジェクトに関連付けられておらず、単独で呼び出された場合、this はモードによって次のように変化します。

  • strict mode
    • this はグローバルオブジェクトを指します。
      つまり、ウェブブラウザであれば Window、ウェブワーカーAPI内なら WorkerGlobalScope、Node.js なら global です。
  • strict mode
    • thisundefined になります。
非strict mode
function showThis() {
  console.log(this);
}

showThis();
デベロッパーツールのコンソール
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
非strict mode
"use strict";

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

showThis();
デベロッパーツールのコンソール
> undefined

オブジェクトに関連付けての呼び出し

関数宣言(function宣言)で作成された関数も、オブジェクトのメソッドとして呼び出される場合があります。

その場合、関数内のthisであっても、呼び出したオブジェクトを指します。

関数がオブジェクトに関連付けられている例
// 👇通常の関数宣言として定義されている
function showThis() {
  console.log(this);
}

const obj = {
  // showThis関数をオブジェクトのメソッドとして関連付け
  showThisMethod: showThis
}

// オブジェクトのメソッドとして関数が呼び出される
obj.showThisMethod();
デベロッパーツールのコンソール
▶ {showThisMethod: ƒ}

コールバック関数内の this

コールバック関数は、他の関数に引数として渡されることも多く、特定のイベントが発生したときや非同期処理が完了したタイミングで呼び出されます。この際、コールバック関数内での this が期待したオブジェクトを指さない場合があります。

そのため、コールバック関数は後述の「アロー関数」で定義することをお勧めします。

3. メソッド内の this

this がメソッド内で使用された場合、基本的にはそのメソッドが属するオブジェクトを指します。

ただし、メソッドが別の変数に代入された場合や、メソッド内にコールバック関数がある場合は異なるため注意が必要です。

基本的な例

基本的なメソッド内の this
const person = {
  name: "Obi-wan Kenobi",
  showThis: function() {
    console.log( this );
  }
};

person.showThis();
デベロッパーツールのコンソール
▶ {name: 'Obi-wan Kenobi', showThis: ƒ}

メソッドが別の変数に代入された場合の this

変数に代入されたメソッド内の this
const person = {
  name: "Obi-wan Kenobi",
  showThis: function() {
    console.log( this );
  }
};

const func = person.showThis;

func();

この場合、単独の func 関数として実行されるため、「2. 関数内での this」の「単独での呼び出し」と同様です。

こういった使い方(メソッドを変数に代入して呼び出す)は次の3つの理由によりお勧めしません。

  1. 可読性の低下: メソッドを変数に代入すると、this の指す対象が変わるため、コードの挙動が直感的に理解しにくくなります。可読性が低下し、保守が困難になることがあります。
  2. デバッグの難しさ: this が変わることで、予期しないバグが発生しやすくなります。特に大規模なプロジェクトでは、デバッグに多くの時間がかかる可能性があります。
  3. 一貫性の欠如: 他の開発者がコードを読んだときに、this がどのオブジェクトを指しているのかを一貫して理解するのが難しくなります。これにより、チーム全体の生産性が低下することがあります。

コールバック関数内の this

コールバック関数は、他の関数に引数として渡されることも多く、特定のイベントが発生したときや非同期処理が完了したタイミングで呼び出されます。この際、コールバック関数内での this が期待したオブジェクトを指さない場合があります。

そのため、コールバック関数は後述の「アロー関数」で定義することをお勧めします。

4. アロー関数内での this

JavaScriptのアロー関数は、関数の定義を簡潔にするだけでなく、this の扱いを大幅に改善します。
特に、後述の「コールバック関数内の this」による混乱を避けるために非常に有用です。

アロー関数とは

アロー関数は、ES6(ECMAScript 2015)で導入された関数の新しい記法です。
従来の関数定義と比べてシンプルに記述でき、特にコールバック関数や短い関数定義に適しています。

アロー関数内の this の特徴

アロー関数の最大の特徴の一つは、外側のスコープから this を継承するという点です。これは、通常の関数と異なり、アロー関数内での this が外側の this と同じであることを意味します。

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

以下に、コールバック関数を通常の関数とアロー関数で作成し、それぞれの this の挙動の違いを例にして、サンプルプログラムで示します。

関数定義として作成したコールバック関数の this
const obj = {
  name: 'Luke',
  func: function() {
     // Luke
    console.log(this.name);
     // 👇undefined (strict mode) または グローバルオブジェクト (非strict mode)
    setTimeout(function() {
      console.log(this.name);
    }, 1000);
  }
};

obj.func();
アロー関数として作成したコールバック関数の this
const obj = {
  name: 'Luke',
  func: function() {
     // Luke
    console.log(this.name);
     // 👇Luke
    setTimeout(() => {
      console.log(this.name);
    }, 1000);
  }
};

obj.func();

このように、アロー関数内の this は外側のスコープから継承されているため、setTimeout 内でも this.nameLuke を指しています。

アロー関数内の this の注意点

  • this のバインディング:
    • アロー関数は自身の this を持たず、外側のスコープから継承するため、クラスのメソッドとして定義する場合に注意が必要です。
    • イベントリスナーもコールバック関数として動作するため、通常関数の要領で this を使用すると意図と異なる挙動になるため注意が必要です。

イベントリスナーでの this

イベントリスナーに登録する関数を、通常関数とアロー関数でそれぞれ作成し、挙動の違いを確認してみましょう。

イベントリスナーに関数宣言を登録するサンプル
const button = document.querySelector("button");
button.addEventListener("click", function(event) {
  console.log( this );
  console.log( event.target );
});
デベロッパーツールのコンソール
> <button>Click!</button>
> <button>Click!</button>

イベントリスナーの第二引数に関数宣言を登録した場合、thisとevent.target が同一のものを指していることが分かります。

イベントリスナーにアロー関数を登録するサンプル
const button = document.querySelector("button");
button.addEventListener("click", (event) => {
  console.log( this );
  console.log( event.target );
});
デベロッパーツールのコンソール
▶ Window {window: Window, self: Window, document: document, name: '', location: Location, …}
> <button>Click!</button>

イベントリスナーの第二引数にアロー関数を登録した場合、thisの方がグローバルオブジェクトを指していることが分かります。

5. コンストラクタ関数内の this

コンストラクタ関数内では、this は新しく作成されるインスタンスを指します。

コンストラクタ関数とは

コンストラクタ関数は、オブジェクトを生成するための特別な関数です。
通常、new キーワードと共に使用され、新しいオブジェクトのプロパティやメソッドを初期化する役割を持ちます。

最も重要な仕事として、作成したインスタンスを返します

基本的なコンストラクタ関数のサンプル

コンストラクタ関数サンプル
function Person(name, age) {
  this.name = name; 
  this.age = age;
}

const jedi = new Person("Luke", 20);
console.log(jedi.name); // 出力: "Luke"

この例では、Person 関数がコンストラクタとして定義され、new キーワードと共に呼び出されています。this は、新しく生成されるオブジェクト(この場合は jedi)を指します。

コンストラクタ関数の注意点

コンストラクタ関数内での this の挙動にはいくつかの注意点があります。

  • 誤って new を使わずコンストラクタ関数呼び出した場合
    • 通常関数として呼び出されてしまうため、this がグローバルオブジェクトを指します。これは意図しないグローバル変数の作成やエラーが発生するため注意が必要です。
  • プロトタイプチェーン
    • コンストラクタ関数で生成されたオブジェクトは、プロトタイプチェーンを介してメソッドやプロパティを継承します。

クラス構文でのコンストラクタ

ES6(ECMAScript 2015)で導入されたクラス構文でも、コンストラクタを使用してオブジェクトを初期化することができます。

クラス構文でのコンストラクタのサンプル
class Person {
  constructor(name) {
    this.name = name; 
  }

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

const jedi = new Person("Luke");
jedi.showThis(); // 出力: Person {name: 'Luke'}

6. callapply、および bind メソッドによる this の明示的な設定

callapply、および bind メソッドは、this を明示的に設定するための強力なツールでした。
しかし、以下の理由で徐々にアンチパターンになりつつあります。

call、apply、bind メソッドの代替案が豊富にある

  • アロー関数の登場により、コールバック関数や非同期処理で this を明示的にバインドする必要がなくなった。
  • 通常関数内においても、this の代わりに self を使用することでシンプルに記述できる。
  • 「部分適用」に使用できるが、「デフォルト引数」を使った方がシンプルに記述できる。

call、apply、bind メソッドにはデメリットが多い

  • 可読性の低下
    • これらのメソッドを多用すると、コードの可読性が低下する可能性があります。特に、関数の呼び出しや this の設定が複雑になると、他の開発者がコードを理解するのが難しくなります。
  • デバッグの難しさ
    • this の文脈が複数のメソッドで設定されると、デバッグが複雑になります。特に、大規模なプロジェクトでは this の設定が追跡しにくくなり、バグの原因を特定するのが難しくなることがあります。
  • パフォーマンスの低下
    • bind メソッドは、新しい関数オブジェクトを生成するため、頻繁に使用するとパフォーマンスが低下する可能性があります。特にパフォーマンスが重要な場合には注意が必要です。
  • 関数の再利用性の低下
    • bindthis を固定すると、その関数は特定のコンテキストでしか使えなくなり、再利用性が低下します。これにより、コードの柔軟性が損なわれる可能性があります。
  • スコープの複雑化
    • callapply を使用して関数を異なるコンテキストで呼び出すと、スコープが複雑になり、変数の参照や this の設定が難しくなることがあります。これが原因で意図しない副作用が発生することもあります。

callapply、および bind メソッドの基本的な使い方

使用頻度は下がっているとはいえ、過去に作成されたプログラムでこれらを目にした際などに、意味がわからないと困る場合もあるでしょう。

それぞれのメソッドの基本的な構文と使い方をご紹介しておきます。

call メソッド

call メソッドは、関数を呼び出す際に this の値を明示的に設定し、個別の引数を渡すことができます。

callメソッドでthisを明示的に設定し、個別の引数を渡すサンプル
function greet(greeting, punctuation) {
  console.log(`${greeting}, my name is ${this.name}!`);
}

const person = {
  name: 'Alice'
};

greet.call(person, 'Hello'); // 出力:"Hello, my name is Alice!"

また、「関数の借用」と呼ばれる使い方ができます。

callメソッドでの「関数の借用」サンプル
const person = {
  name: 'Luke',
  greet: function(greeting) {
      console.log(`${greeting}, my name is ${this.name}`);
  }
};

const anotherPerson = {
  name: 'Yoda'
};

person.greet.call(anotherPerson, 'Hello'); // 出力:"Hello, my name is Yoda"

apply メソッド

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

applyメソッドでthisを明示的に設定し、引数を配列で渡すサンプル
function greet(greeting, mark) {
  console.log(`${greeting}, my name is ${this.name}${mark}`);
}

const person = {
  name: 'Alice'
};

greet.apply(person, ['Hello', '!']); // "Hello, my name is Alice!"

bind メソッド

bind メソッドは、関数の this を特定のオブジェクトに固定し、新しい関数を返します。

bindメソッドでthisを特定オブジェクトに固定し、新たな関数を返すサンプル
function greet(greeting, mark) {
  console.log(`${greeting}, my name is ${this.name}${mark}`);
}

const person = {
  name: 'Alice'
};

const greetPerson = greet.bind(person, 'Hello', '!');
greetPerson(); // "Hello, my name is Alice!"

7. ES6 のクラスで super を使用する場合の this

ES6(ECMAScript 2015)で導入されたクラス構文は、JavaScriptでオブジェクト指向プログラミングを行うためのより直感的な方法を提供します。
特に、クラス継承の際に使用される super キーワードは、親クラスのコンストラクタやメソッドにアクセスするための強力なツールです。

この項では、クラス内で使用される this について説明していきます。

メソッド内での thissuper

メソッド内では、this は常にそのメソッドを持つクラスのインスタンスを指します。
super を使うことで、親クラスのメソッドにアクセスすることができます。

クラスのメソッド内での this と super のサンプル
class Dog extends Animal {
  speak() {
      super.speak(); // 親クラスのメソッドを呼び出す
      console.log(`${this.name} barks.`); // `this` は Dog インスタンスを指す
  }
}

コンストラクタ内での thissuper

継承クラスのコンストラクタ内では、this を使用する前に必ず super を呼び出さなければなりません。
これにより、親クラスのコンストラクタが適切に初期化されます。

クラスのコンストラクタ内での this と super のサンプル
class Dog extends Animal {
    constructor(name, breed) {
        // superを呼び出す前にthisを使うとエラーが発生します
        super(name); // まず親クラスのコンストラクタを呼び出す
        this.breed = breed; // その後でthisを使用する
    }
}

まとめ

今回の記事ではJavaScriptの this について詳しく見てきましたが、非常にボリュームがある上に内容も難解なので、一度にすべて記憶・理解しようとする必要はありません。

忘れるたびに繰り返し復習し、徐々に記憶を定着させ、理解を深めていきましょう。


本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。

それでは、最後までお読みいただきありがとうございます。

『詳説 JavaScript』メニューに戻る

コメント

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