JavaScript入門 thisキーワード
JavaScriptには、オブジェクトへの参照を格納するthisという特殊なキーワードがあります。thisキーワードについて解説しています。
実行コンテキスト
JavaScriptのthisキーワードを理解するには、まず実行コンテキストについて知っておく必要があります。実行コンテキストとは、コードが実行される際にJavaScriptエンジンによって準備されるコードの実行環境のことです。JavaScriptのコードが実行される前に必ず実行コンテキストが生成され、どのような状態でコードが実行されているのかという情報が実行コンテキストごとに保持されます。
実行コンテキストが生成されるタイミングは、主に2種類あります。
- HTMLのscriptタグの直下やJavaScriptファイルの直下に記述されたコードが実行される直前
- 関数が実行される直前
1、ではグローバルコンテキストと呼ばれる実行コンテキストが生成され、2、では関数コンテキストと呼ばれる実行コンテキストが生成されます。コード内で関数を実行すると複数の実行コンテキストが生成され、実行コンテキストごとに次のような情報が保持されます。
- 「その実行コンテキスト内で宣言された変数や関数」または「レキシカルスコープの変数や関数」
- 「thisの参照先」や「その他の使用可能な変数やキーワード」
そんとあめ、各々の実行コンテキストが保持する内容(変数や関数、thisの参照先など)は異なります。ここで一番重要なのは、実行コンテキストが変わるとthisの参照先の値も変わるということです。
グローバルコンテキスト
グローバルコンテキストは、トップレベルでコードを実行する前に生成される実行コンテキストです。すなわち、HTMLのscriptタグの直下やJavaScriptファイルの直下に記述されたコードが実行される前に、まずグローバルコンテキストが生成されます。グローバルコンテキストでは、同コンテキスト内で宣言した変数や関数以外にも、Windowオブジェクト(window)とthisが使用可能な状態になります。
Windowオブジェクト
Windowオブジェクト(window)は、これまで使ってきたNumberやString、consoleやsetTimeoutなど、ブラウザがあらかじめ用意してくれているオブジェクトと関数(Web API)をプロパティに持つ特殊なオブジェクト(グローバルオブジェクト)です。Windowオブジェクトは、コードのどこからでも参照でき、またwindow.の部分を省略して記述することができます。
this
thisキーワードは実行コンテキストによって参照先の値が切り替わりますが、グローバルコンテキストのthisはグローバルオブジェクト(Windowオブジェクト)を参照します。
<script>
/* トップレベルで実行した場合のthisはWindowオブジェクトを参照する */
console.log( this === window )
> true
</script>
globalThisとは?
実行環境に応じたグローバルオブジェクトを取得するための識別子です。グローバルオブジェクトは、JavaScriptの実行環境によって変わります。たとえば、ブラウザ環境の場合のグローバルオブジェクトは、Windowオブジェクトです。しかし、ブラウザ内部のWeb WorkerやService Workerと呼ばれる特殊な実行環境ではJavaScriptコードが実行された場合、グローバルオブジェクトは、selfという識別子になります。また、Node.js環境でコードが実行された場合、グローバルオブジェクトはglobalという識別子になります。このような違いは、環境をまたいで動作するコードを記述する場合とても面倒です。そのため、ES2022バージョンから、globalThisというキーワードによって、JavaScriptが実行される環境に応じたグローバルオブジェクトを取得できるようになりました。
関数コンテキスト
関数コンテキストは、関数が実行さえるときに生成される実行コンテキストです。関数コンテキスト内で宣言した変数や関数以外にも、レキシカルスコープの変数やthisキーワード、argumentsオブジェクトやsuperという特殊なキーワードが使用できます。なお、superは、クラス継承が行われる特殊な条件下でのみ利用可能なキーワードです。
コールスタック
コールスタックとは、実行コンテキストの積み重ねのことです。グローバルコンテキストや関数コンテキストは、同コンテキスト内のコードの実行をすべて完了したタイミングで消滅しますが、同コンテキスト内のコードで他の関数が呼びされると呼び出し元のコンテキストの上に積み上げられる形で関数コンテキストが新たに作成されます。これが繰り返されたときには、JavaScriptエンジン上でどんどん関数コンテキストが積み上がっていく状態になります。これをコールスタックと呼びます。
上記のコードでは、まずトップレベルのコード実行前(①)にグローバルコンテキストが生成されます。そして、②でfirst()が実行されたときに関数コンテキストがグローバルコンテキストの上に積み上がります。そのして、次にfirst()内でsecond()が実行されている(③)ため、さらに関数コンテキストが積み上がります。second()の中では他の関数は呼び出されないので、これ以上コンテキストは積み上がりません。second()の実行が完了するとsecond()の関数コンテキストが消滅し(④)、あとは同様に実行が終了したコンテキストから消滅(⑤⑥)していきます。これがコールスタックの動作です
ブラウザのコールスタック
ブラウザは、コールスタックを保持することで、実行コンテキストごとのthisの参照先やコンテキスト内で宣言された変数の情報を保持しています。
<script>
function first(){
let firstVal = "firstコンテキスト";
second();
}
function second(){
let secondVal = "secondコンテキスト";
debugger;
}
first();
</script>
※debuggerというキーワードは、開発ツールを開いた状態でコードを実行したときに、コードの実行を停止します。
開発ツールの「Sources」パネルを開き、右下の「Call Stack」の欄でコールスタックを確認できます。たとえば、ここでfirstの欄をクリックすると、関数firstの実行コンテキストのthisや変数を確認できます。コールスタックは、コードのどこから呼び出されて、どのような状態で実行されたのか知るために必要な手段です。
関数コンテキストのthisの挙動
1、関数コンテキストのthisの種類
関数コンテキスト内のthisは、関数の実行の仕方によって、参照する先の値がことなります。これは、大きく2種類に分類できます。
1、オブジェクトのメソッドとして実行したい場合
thisの参照先は、メソッドが格納されているオブジェクトです。「メソッドとして実行する」とは、つまり「オブジェクト.メソッド()」の形式で実行することです。
2、関数として実行した場合
thisの参照先は、グローバルオブジェクト(ブラウザの場合はWindowオブジェクト)です。
2、オブジェクトのメソッドとして実行した場合
オブジェクトのメソッドとして実行される関数内のthisの参照先は、メソッドが呼び出される元になったオブジェクト(ドット演算子の前のオブジェクト)です。次のコードでは、メソッドhelloを実行したときの呼び出し元オブジェクトはtaroです。そして、メソッドhello内のthisは、このtaroを参照しています。
const taro = {
name: "太郎",
hello: function(){
console.log( "こんにちは" + this.name );
}
}
taro.hello();
>こんにちは、太郎
メソッドは、オブジェクトに格納されている関数のことを指します。taro.hello()のように関数を事項下場合、オブジェクト(taro)のメソッドつぉいて関数(hello)を実行していることを意味sます。
関数として実行した場合
一方、オブジェクトのメソッドではなく、ただの関数として実行された場合のthisの参照先は、Windowオブジェクトです。
windows.name = "花子";
function hello(){
console.log( "こんにちは" + this.name );
}
hello();
>こんにちは、花子
そのため、上記コードの関数helloを実行したときに、this.nameプロパティで取得されるのは、Windowオブジェクトのnameプロパティに格納した”花子”になります。
"use strict"; //Strictモードを有効化
function confirmThis() { console.log( this ); } //tihsの値を確認
confirmThis();
>undefined //Windowオブジェクトではなくundefinedになる
アロー関数内でthisが使われた場合の挙動
アロー関数の特徴として、アロー関数が実行された時の関数コンテキストには、thisが存在しません。そのため、アロー関数内でthisキーワードが使われた場合には、スコープチェーンをたどって、レキシカルスコープに対してthisを探しに行きます。そして、そのとき最初に見つかったthisが、アロー関数のthisキーワードの参照先として使われます。次のような場合、アロー関数arrowFn内でthisの値を取得しようとすると、1つの外側の関数fnのthisの値が取得されます。
const taro = {
name: "太郎",
hello: function(){
const jiro = {
name:"",
hello:() => {
console.log( "こんにちは" + this.name );
}
};
jiro.hello();
}
};
taro.hello();
>こんにちは、太郎
taro.helloは無名関数、jiro.helloはアロー関数を使ってメソッドを定義しています。このとき、アロー関数であるjiro.helloは自身の関数コンテキストを持たないため、レキシカルスコープのtaro.helloのthisを使います。このthisはtaroオブジェクトへの参照となるため、結果として「こんにちは、太郎」がコンソールに出力されます。
メソッドの省略記法
オブジェクトリテラル内のメソッドの省略記法は、無名関数のメソッド定義と同じ意味になります。
const ojb = {
method(){} //これはmethod: function() {}と定義した場合と同じ
}
コールバック関数におけるthisの参照先
オブジェクトのメソッドをコールバック関数として異なる関数に渡したときの挙動
window.name = "花子";
const taro = {
name: "太郎",
hello: function(){
console.log("こんにちは、" + this.name);
}
}
function greeting( callback ){
callback(); //callback関数(taro.hello)がこの時点で実行される
}
greeting( taro.hello ); //taro.helloを引数に渡す(この時点ではまだ実行されていない)
>こんにちは、花子
上記コードを実行すると、コンソールには「こんにちは、花子」と表示されます。taro.helloの関数を実行しているため、一見、taro.nameの値が取得されそうですが、実施はwindow.nameの値が取得されます。これは、上記コードではtaro.helloが参照している先の関数がcallbackとして引数に渡り、callback()という形で実行されているからです。このとき、関数が「オブジェクト.メソッド()」の形式で実行されていません。callback()はあくまで関数として実行されているため、thisの参照先はWindowオブジェクトになります。
同様のことは、オブジェクトのメソッドを変数に代入したときにも発生します
window.name = "花子";
const taro = {
name: "太郎",
hello: function(){
console.log("こんにちは、" + this.name);
}
}
const hellWho = taro.hello; //メソッドを変数に格納
helloWho(); //関数として実行される
>こんにちは、花子
この場合もhelloWho()は「オブジェクト.メソッド()」の形式では実行されておらず、関数として実行されているため、thisはWindowオブジェクトを参照しています。
thisの束縛
thisの参照先は切り替わりますが、開発者が束縛する方法もあります。thisを特定の固定(束縛)するには、bind、apply、callという3つのメソッドを使います。
bindメソッド
bindメソッドを使うと、thisの参照先の値を自由に変更できます。bindを使ってthisの参照先を変更することを、bindによるthisの束縛と言います。
構文:bindの記法
const newFn = fn.bind( obj [, param1 , param2, ・・・] )
- fn :this、または引数を束縛したい関数かメソッドを指定します。
- obj :関数fn内のthisの参照先にしたいオブジェクトを指定します。
- param1、param2:bindでは関数fnに渡す引数も指定できます。
- newFn:bindによって、thisまたは引数が束縛された、新しい関数が格納されます。
bindによってthisを束縛するときには、関数fnに続けて.bind(束縛したいオブジェクト)を記述します。これによって、関数fnのthisの参照先がbindメソッドの第1引数のオブジェクトに束縛された、新たな関数がnewFnに格納されます。このとき、bindメソッドは、関数fnを実行しているわけではありません。あくまでthisの参照先を固定した新しい関数(newFn)を作成しているだけです。
bindを利用してthisを束縛
function hello( greeting ){
console.log( greeting + this.name );
}
const taro = {
name: "太郎"
}
//bindでthisと引数を束縛
const helloTaro = hello.bind( taro, "こんにちは、" );
helloTaro();
>こんにちは、太郎
bindメソッドで新たに作成された関数helloTaroのthisと引数は、bindメソッドの第1引数、第2引数でそれぞれ固定されることになります。そのため、helloTaroを実行すると、コンソールに「こんにちは、太郎」と表示されます。
bindメソッドの利用ケース
bindメソッドが主に使われるのは、コールバック関数として渡す関数のthisや引数を束縛したい場合です。たとえば、オブジェクトのメソッドをコールバック関数として渡した場合、関数として実行されてしまうため、thisの参照先がWindowオブジェクトとなってしまいます。しかし、bindメソッドを使うことで、thisの参照先のオブジェクトを開発者の意図したものに指定できます。
window.name = "花子";
const taro = {
name: "太郎",
hello: function(){
console.log("こんにちは、" + this.name);
}
}
//2秒後にこんにちは、太郎とコンソールに表示される
setTimeout( taro.hello.bind( taro ),2000 ); //bindでthisをtaroに固定
//3秒後にこんにちは、花子とコンソールに表示される
setTimeout( taro.hello , 3000); //bindなし
setTimeoutにtaro.helloをただ渡しただけでは、thisの参照先がWindowオブジェクトになってしまいます。bindでthisの参照先をtaroオブジェクトにすることによって、setTimeout内でコールバック関数が実行されたときにthisの参照先をtaroオブジェクトに束縛します。
この書き方が難しいと感じる場合は、別の書き方のあります。
window.name = "花子";
const taro = {
name: "太郎",
hello: function(){
console.log("こんにちは、" + this.name);
}
}
//無名関数でtaro.hello();を囲む
setTimeout(
function() {
taro.hello(); //※1
},2000 ); //2秒後に以下のメッセージが表示される
>こんにちは、太郎
このように無名関数でtaro.hello()をラップした(囲む)場合に※1のtaro.helloメソッドの実行部分が「オブジェクト.メソッド()」の形式で記述できます。これによってsetTimeout内でcallback()として実行されるのは、ラップした無名関数になるため、その中でtaro.hello()はオブジェクトのメソッドとして実行されることになります。
callメソッド
bindメソッドでは、新しい関数を生成するだけで、その関数は実行しません。しかし、callメソッドでは、thisや引数を束縛した、新しい関数を作成しその関数を即時に実行します。
利用度は低いです
構文:callの記法
fn.call( obj [, param1, param2, ・・・] );
- fn :this、または引数を束縛したい関数かメソッドを指定します。
- obj :関数fn内のthisの参照先にしたいオブジェクトを指定します。
- param1、param2:callでは関数fnに渡す引数も指定できます。
callメソッドは、bindメソッドと同じ形式で引数を渡します。唯一、bindメソッドと異なるのは、callメソッドを呼び出した時点で、thisや引数が束縛された関数が実行される点です。
const taro = { name: "太郎" };
function hello(greeting){
console.log( `${ greeting }、 ${ this.name }` );
}
hello.call( taro, "こんにちは" ); //thisと引数を束縛して関数helloを実行
>こんにちは、太郎
applyメソッド
applyメソッドは、callメソッドと同じく、thisや引数を束縛して新しい関数を作成し、その関数を即時に実行します。callメソッドと異なるのは、applyメソッドでは、引数の束縛に配列を使うという点です。
利用度は低いです。
構文:applyの記法
fn.apply( obj [, array] );
- fn :this、または引数を束縛したい関数かメソッドを指定します。
- obj :関数fn内のthisの参照先にしたいオブジェクトを指定します。
- array:applyメソッドの第2引数には配列を指定します。配列に格納された要素が関数fnの引数としてそれぞれ渡されます。
使用例:applyメソッド
const taro = { name: "太郎" };
function hello( greeting, name ){
console.log( `${ greeting }、 ${ this.name }` );
}
hello.apply( null, [ "こんにちは", "太郎" ] ); //thisと引数を束縛して関数helloを実行
>こんにちは、太郎
関数helloの引数greeitinとnameを、配列0番目(”こんにちは”)と1番目(”太郎”)の要素で、それぞれ束縛しています。また、このとき関数hello内ではthisを使っていないので、applyメソッドの第1引数にnullを設定しています。
別の例として、Mathオブジェクトのmaxメソッドでは、引数に渡された値の中で一番大きな値を戻り値として返してますが、比較する値を引数に1つずつ指定する必要があります。そのため、仮に配列内の一番大きな数値を取得したい場合には、次のように記述する必要があります。
const vals = [1,2,3];
//一番大きな数値を確認
console.log( Math.max(vals[0], vals[1], vals[2] ) );
>3
applyメソッドを使うと、以下のように短縮できます。※ES6で追加されたスプレット演算子を使うと同様のことができるため、あまり使われません。
const vals = [1,2,3];
//一番大きな数値を確認
console.log( Math.max.apply( null, vals ) ); //thisは使用しないため、nullを第1引数に渡す
>3