技術とか戦略とか

SIerで証券レガシーシステムを8年いじってからSESに転職した普通の文系SEによる技術ブログ。

JavaScript:thisの挙動と用途

JavaScriptの初心者を悩ます文法の一つとして、thisが挙げられます。
thisが何を指しているのか、どのような場合に使われるのか、なかなかイメージが難しいと思います。
 
この記事では、その疑問について答えていきたいと思います。
 
今回の記事では、サンプルコードの実行はNode.jsで行います。
また、非Strictモード('use strict'を宣言したりexport句でStrictモードに変更したりしない場合)を前提とします。
(Strictモードの場合は、thisによりグローバルオブジェクトを参照しようとした場合にundefinedになります)
 
----
 
1.thisキーワードの説明
「this」の説明の前に、JavaScriptとオブジェクトの関係について簡単に説明します。
JavaScriptは、必ずオブジェクトの中で実行する必要があります。
実行環境(ブラウザやNode.js)は「グローバルオブジェクト」と呼ばれるオブジェクトを用意しており、自分でオブジェクトを定義しない場合はグローバルオブジェクトから各々の処理が実行されています。
(ブラウザの場合のグローバルオブジェクトは「window」、Node.jsの場合のグローバルオブジェクトは「global」となります)
 
「this」キーワードは、記述箇所を動かしているオブジェクトを参照します。
自分で定義したオブジェクトから記述箇所を呼び出している場合はそのオブジェクト、そうではない場合はグローバルオブジェクトを返します。
 
【サンプルコード】
function func1() {
 console.log(this); // 関数を呼び出しているオブジェクトを参照
}
func1(); // グローバルオブジェクトの関数を実行
let obj = {}; // 独自のオブジェクト定義
obj.func = func1; // 独自のオブジェクトに関数を定義
obj.func(); // 独自のオブジェクトの関数を実行
 
【実行結果】
・1回目の関数呼び出し
Object [global] {
 global: [Circular],
 clearInterval: [Function: clearInterval],
 clearTimeout: [Function: clearTimeout],
 setInterval: [Function: setInterval],
 setTimeout: [Function: setTimeout] {
  [Symbol(nodejs.util.promisify.custom)]: [Function]
 },
 queueMicrotask: [Function: queueMicrotask],
 clearImmediate: [Function: clearImmediate],
 setImmediate: [Function: setImmediate] {
  [Symbol(nodejs.util.promisify.custom)]: [Function]
 }
}
 
・2回目の関数呼び出し
{ func: [Function: func1] }
 
2.callやapplyを用いて関数呼び出しした場合のthisの参照先
関数をメソッドチェーンで「call」や「apply」と繋ぐことで、呼び出される関数の中でthisが参照するオブジェクトを指定することができます。
オブジェクトの指定は「call」や「apply」の第一引数で行います。
 
また、「call」や「apply」は第二引数以降も指定が可能です。
第二引数以降では、関数呼び出しの際の引数を指定することができ、呼び出される関数の中では「arguments」キーワードでそれを受け取ることができます。
「call」は第二引数、第三引数、第四引数…といった具合に変数を一つずつ定義し、「apply」は第二引数に配列形式でまとめて変数を定義します。
 
【サンプルコード1】
function func1() {
 console.log(this);
}
let obj = {hoge: "hoge"};
func1.call(obj); // this=objと結びつけることができる。
func1.apply(obj); // 同上
 
【実行結果1】
・1回目の関数呼び出し
{ hoge: 'hoge' }
 
・2回目の関数呼び出し
{ hoge: 'hoge' }
 
【サンプルコード2】
function func1() {
 console.log(this);
 for (var i = 0; i < arguments.length; i++) {
  console.log(arguments[i]);
 };
}
let obj = {hoge: "hoge"};
func1.call(obj, "Hello", "World!"); // 引数を1つずつ指定
func1.apply(obj, ["Hello", "World!"]); // 引数を1つの配列で指定
 
【実行結果2】
・1回目の関数呼び出し
{ hoge: 'hoge' }
Hello
World!
 
・2回目の関数呼び出し
{ hoge: 'hoge' }
Hello
World!
 
3.bindを用いたthisの参照先変更
関数をメソッドチェーンで「bind」と繋ぐことで、thisの参照先を強制的に結びつけた新たな関数を定義できます。
結びつけるオブジェクトは第一引数で定義します。
 
【サンプルコード】
function func1() {
 console.log(this);
}
let obj = {hoge: "hoge"};
let func2 = func1.bind(obj); // thisを強制的に結び付けて新たな関数を定義
func2();
 
【実行結果】
{ hoge: 'hoge' }
 
4.アロー関数で関数定義した場合のthisの参照先
アロー関数で関数を定義した場合は、thisの参照先は固定になります。
関数を定義した時点のオブジェクトを指すようになります。
 
【サンプルコード】
let func1 = () => { // アロー演算子で関数を定義
 console.log(this); // この場合は、定義時点の呼び出し元オブジェクトでthisを決定
}
func1(); // グローバルオブジェクトから呼び出してもthisは空のオブジェクトを指す
let obj = {};
obj.func = func1;
obj.func(); // 独自のオブジェクトから呼び出してもthisは空のオブジェクトを指す
 
【実行結果】
・1回目の関数呼び出し
{}
 
・2回目の関数呼び出し
{}
 
----
 
thisは、どのオブジェクトから呼び出されたかによって挙動を変えたい時に使用することが多いです。
ポリモーフィズムを実現するために使用する、と言った方がわかりやすいかもしれません。
また、今回の例では取り上げていませんが、関数を定義したオブジェクトを渡せばコールバック処理も可能です。
 
以下のサンプルコードは、呼び出したオブジェクトの名前を参照する例であり、どのオブジェクトから呼び出したかによって挙動を変えることができています。
 
【サンプルコード】
function func1() {
 console.log(this.name); // 呼び出したオブジェクト毎で違う名前を表示
}
let obj1 = {name: "hoge"};
let obj2 = {name: "fuga"};
func1.call(obj1);
func1.call(obj2);
 
【実行結果】
・1回目の関数呼び出し
hoge
 
・2回目の関数呼び出し
fuga