Javaなど他のプログラミング言語にもthisは存在しますが、JavaScriptのthisは「どのように呼び出されるか」によって値が決まるため、完全に理解して使いこなすのが難しいと言われがちです。
そこで本記事では、JavaScriptのthisについて、ワンステップずつ理解を深められるよう説明を進めていきます。
一気にすべてを記憶・理解しようと焦らないでください。
この機会に、使用パターンごとの挙動を1つ1つ着実に理解し、サンプルを手を動かして実装しながら体得頂ければと思います。
目次
thisの使用パターン一覧
まずは、パターンの全量を一覧化し、それぞれに簡単な説明を補足します。
- グローバルコンテキストの
this
- 関数内の
this
- メソッド内の
this
- アロー関数内の
this
- コンストラクタ関数内の
this
call
、apply
、およびbind
メソッドによるthis
の明示的な設定- ES6 のクラスで
super
を使用する場合のthis
No.5までは、完全に理解頂きたい内容になっています。
No.6以降は、「thisの匠」レベルに極めたい方は頑張って理解してください。
1. グローバルコンテキストの this
this がグローバルスコープで直接使われるケースはそう多くありませんが、グローバルオブジェクトを理解するためにリストアップしています。
console.log( this );
このように、グローバルスコープで使用された場合、thisは「グローバルオブジェクト」を指します。
ただし、strict modeで実行された場合には undefined を指すのでご注意ください。
- 非strict mode:
this
はグローバルオブジェクトを指します。
- strict mode:
this
はundefined
になります。
グローバルオブジェクトは、JavaScriptがどのような環境で実行されるかによって変化します。
環境によるグローバルオブジェクトの変化
- ウェブブラウザーで実行された場合:
- グローバルオブジェクトは Window です。ウェブにおける JavaScript コードのほとんどはこのケースに該当しますが、後述のウェブワーカー内では異なります。
- ウェブワーカー API内で実行された場合:
- グローバルオブジェクトは WorkerGlobalScope です。また、この場合、
self
キーワードもWorkerGlobalScope
を指します。
- グローバルオブジェクトは WorkerGlobalScope です。また、この場合、
- Node.js で実行された場合:
- グローバルオブジェクトは global と呼ばれるオブジェクトです。
2. 関数内の this
関数宣言(function宣言)で作成された関数内の this は、どのように呼び出されるかによって指し示すものが変化します。
単独での呼び出し
関数がオブジェクトに関連付けられておらず、単独で呼び出された場合、this はモードによって次のように変化します。
- 非strict mode:
this
はグローバルオブジェクトを指します。
つまり、ウェブブラウザであれば Window、ウェブワーカーAPI内なら WorkerGlobalScope、Node.js なら global です。
- strict mode:
this
はundefined
になります。
function showThis() {
console.log(this);
}
showThis();
▶ Window {window: Window, self: Window, document: document, name: '', location: Location, …}
"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 がメソッド内で使用された場合、基本的にはそのメソッドが属するオブジェクトを指します。
ただし、メソッドが別の変数に代入された場合や、メソッド内にコールバック関数がある場合は異なるため注意が必要です。
基本的な例
const person = {
name: "Obi-wan Kenobi",
showThis: function() {
console.log( this );
}
};
person.showThis();
▶ {name: 'Obi-wan Kenobi', showThis: ƒ}
メソッドが別の変数に代入された場合の this
const person = {
name: "Obi-wan Kenobi",
showThis: function() {
console.log( this );
}
};
const func = person.showThis;
func();
この場合、単独の func 関数として実行されるため、「2. 関数内での this
」の「単独での呼び出し」と同様です。
こういった使い方(メソッドを変数に代入して呼び出す)は次の3つの理由によりお勧めしません。
- 可読性の低下: メソッドを変数に代入すると、
this
の指す対象が変わるため、コードの挙動が直感的に理解しにくくなります。可読性が低下し、保守が困難になることがあります。 - デバッグの難しさ:
this
が変わることで、予期しないバグが発生しやすくなります。特に大規模なプロジェクトでは、デバッグに多くの時間がかかる可能性があります。 - 一貫性の欠如: 他の開発者がコードを読んだときに、
this
がどのオブジェクトを指しているのかを一貫して理解するのが難しくなります。これにより、チーム全体の生産性が低下することがあります。
コールバック関数内の this
コールバック関数は、他の関数に引数として渡されることも多く、特定のイベントが発生したときや非同期処理が完了したタイミングで呼び出されます。この際、コールバック関数内での this
が期待したオブジェクトを指さない場合があります。
そのため、コールバック関数は後述の「アロー関数」で定義することをお勧めします。
4. アロー関数内での this
JavaScriptのアロー関数は、関数の定義を簡潔にするだけでなく、this
の扱いを大幅に改善します。
特に、後述の「コールバック関数内の this
」による混乱を避けるために非常に有用です。
アロー関数とは
アロー関数は、ES6(ECMAScript 2015)で導入された関数の新しい記法です。
従来の関数定義と比べてシンプルに記述でき、特にコールバック関数や短い関数定義に適しています。
アロー関数内の 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();
const obj = {
name: 'Luke',
func: function() {
// Luke
console.log(this.name);
// 👇Luke
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
obj.func();
このように、アロー関数内の this
は外側のスコープから継承されているため、setTimeout
内でも this.name
は Luke を指しています。
アロー関数内の 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. call
、apply
、および bind
メソッドによる this
の明示的な設定
call
、apply
、および bind
メソッドは、this
を明示的に設定するための強力なツールでした。
しかし、以下の理由で徐々にアンチパターンになりつつあります。
call、apply、bind メソッドの代替案が豊富にある
- アロー関数の登場により、コールバック関数や非同期処理で this を明示的にバインドする必要がなくなった。
- 通常関数内においても、this の代わりに self を使用することでシンプルに記述できる。
- 「部分適用」に使用できるが、「デフォルト引数」を使った方がシンプルに記述できる。
call、apply、bind メソッドにはデメリットが多い
- 可読性の低下:
- これらのメソッドを多用すると、コードの可読性が低下する可能性があります。特に、関数の呼び出しや
this
の設定が複雑になると、他の開発者がコードを理解するのが難しくなります。
- これらのメソッドを多用すると、コードの可読性が低下する可能性があります。特に、関数の呼び出しや
- デバッグの難しさ:
this
の文脈が複数のメソッドで設定されると、デバッグが複雑になります。特に、大規模なプロジェクトではthis
の設定が追跡しにくくなり、バグの原因を特定するのが難しくなることがあります。
- パフォーマンスの低下:
bind
メソッドは、新しい関数オブジェクトを生成するため、頻繁に使用するとパフォーマンスが低下する可能性があります。特にパフォーマンスが重要な場合には注意が必要です。
- 関数の再利用性の低下:
bind
でthis
を固定すると、その関数は特定のコンテキストでしか使えなくなり、再利用性が低下します。これにより、コードの柔軟性が損なわれる可能性があります。
- スコープの複雑化:
call
やapply
を使用して関数を異なるコンテキストで呼び出すと、スコープが複雑になり、変数の参照やthis
の設定が難しくなることがあります。これが原因で意図しない副作用が発生することもあります。
call
、apply
、および bind
メソッドの基本的な使い方
使用頻度は下がっているとはいえ、過去に作成されたプログラムでこれらを目にした際などに、意味がわからないと困る場合もあるでしょう。
それぞれのメソッドの基本的な構文と使い方をご紹介しておきます。
call メソッド
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!"
また、「関数の借用」と呼ばれる使い方ができます。
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
を設定しますが、引数を配列として渡します。
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
を特定のオブジェクトに固定し、新しい関数を返します。
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 について説明していきます。
メソッド内での this
と super
メソッド内では、this
は常にそのメソッドを持つクラスのインスタンスを指します。super
を使うことで、親クラスのメソッドにアクセスすることができます。
class Dog extends Animal {
speak() {
super.speak(); // 親クラスのメソッドを呼び出す
console.log(`${this.name} barks.`); // `this` は Dog インスタンスを指す
}
}
コンストラクタ内での this
と super
継承クラスのコンストラクタ内では、this
を使用する前に必ず super
を呼び出さなければなりません。
これにより、親クラスのコンストラクタが適切に初期化されます。
class Dog extends Animal {
constructor(name, breed) {
// superを呼び出す前にthisを使うとエラーが発生します
super(name); // まず親クラスのコンストラクタを呼び出す
this.breed = breed; // その後でthisを使用する
}
}
まとめ
今回の記事ではJavaScriptの this について詳しく見てきましたが、非常にボリュームがある上に内容も難解なので、一度にすべて記憶・理解しようとする必要はありません。
忘れるたびに繰り返し復習し、徐々に記憶を定着させ、理解を深めていきましょう。
本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。
それでは、最後までお読みいただきありがとうございます。
コメント