JavaScriptのthis
JavaScriptの入門時に躓くのがthisです。わからないなりに工夫するとその混乱を回避できたりして、放っておいたのですが、ここで整理します。
MDN:「this」のページに沿って、わかりにくいところを補足しながら理解していきます。
簡潔に説明すると
JavaScriptのthisを簡潔に説明すると、コンテキスト(記述される場所)によってセットされる値が変わる変数です。値には基本的に呼び出し元が入ります。
例えば、object.function()として、呼び出した時functionの中でのthisはobjectを指します。
この原則ルールに対し「厳格(Strict)モード」や「bind/call/apply関数」「アロー関数」などにおいては原則とは違った挙動をするので混乱します。
厳格(Strict)モードは簡単にいえばコード記述における制限やエラー検出を厳しくしてバグを減らそうとするモードです。想定外の挙動になることが減る分、エラーがスローされてコードの実行が止まるケースが増える欠点もあります。
厳格モードはjsファイルの先頭に「'use strict';」と記述することで有効になります。また関数内だけに利用したい場合は関数の先頭にも記述することができます。モジュールやクラスは何も指定しなくても厳格モードになります。
コンテキスト
コンテキストはコードが記述される場所と書きましたが、具体的にどのようなものがあるかというと、「関数」「オブジェクト」「クラス」などがあります。
またすべての関数の外側にある部分を「グローバルコンテキスト」と呼びます。
各コンテキストでの挙動
・グローバル
グローバルコンテキストで記述されるthisはグローバルオブジェクトを指します(ブラウザならwindow、Node.jsならglobal)。
・関数
関数内のthisには呼び出し元がセットされます。関数名だけで実行するとき呼び出し元は存在しませんが、この場合は厳格モード時はundefined、そうでない場合はグローバルオブジェクトがセットされます。
ここまでの違いをまとめると次のようになります。
func1.js
function func1 () {
console.log(this === undefined ? "undefined" : this);
}
console.log(this);//window
func1();//window
window.func1();//window
記述方法 | 厳格モードでのthis | 通常モードでのthis |
---|---|---|
this | window | window |
func1() | undefined | window |
window.func1() | window | window |
・オブジェクトやクラス
オブジェクトやクラス内に記述されるthisは、オブジェクトやクラス自身を指します。
通常通り呼び出すとthisはオブジェクトになります。別に定義した関数をオブジェクトに組み入れればthisはオブジェクトになり、逆に別の関数に取り出せば取り出した場所におけるthisが適用されます。
func2-4.js
var obj ={
param : "object1",
func2: function() {
console.log(this === undefined ? "undefined" : this);
}
}
obj.func3 = func1;//先のfunc1をfunc3としてオブジェクトにセット
let func4 = obj.func2;//オブジェクト内のfunc2をfunc4として新たに定義
obj.func2();//obj
obj.func3();//obj
func4();//window
記述方法 | 厳格モードでのthis | 通常モードでのthis |
---|---|---|
obj.func2() | obj | obj |
obj.func3() | obj | obj |
func4() | undefined | window |
クラスに関してもおおむね同じ挙動ですが、クラスは厳格モードで記述されること(func4と同じようなことをクラスで実行すると通常モードでもundefinedになります)と、派生クラス(extends)ではsuperを呼び出さないとthisが設定されない点がオブジェクトとは違います。
・DOM
addEventListener()によって追加されるDOMのイベントハンドラにおけるthisには、イベントリスナーが配置されている要素が入ります。またインラインの記述においてもthisにはイベントリスナーが配置されている要素が入ります。
domsample.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
</head>
<body>
<button onclick="test1(this)">テスト1</button>
<button id="btn">テスト2</button>
<script>
function test1(t) {
console.log(t);//button要素
console.log(this);//window
}
function test2(e) {
console.log(e);//Event
console.log(this);//button要素
}
document.getElementById('btn').addEventListener('click', test2, false);
</script>
</body></html>
bind/call/apply
bindやcall、apply関数の利用すると、thisの値を任意に変更できるため通常とは違う値がthisに入ることがあります。
bindでは関数名.bind(オブジェクト)とすることで、thisの値を引数に指定したオブジェクトに固定した新たな関数が生成されます。 この時受け取った新たな関数からさらにbindしようとしても無視されます。
callでは第1引数にthisに割り当てたいオブジェクト、第2引数以降に元の関数の引数を渡します。applyはcallで渡すべき第2引数である引数を配列で渡す関数となります。
applyは配列を展開して実行するので、次のようにして配列のマージで利用されることがあるそうです。
push関数の内部ではthisを利用して配列を追加しているため、thisに元のarrayを設定しています。このようにする理由は単に配列を展開したいだけなので、…(スプレッド構文)を使った方がスマートかもしれませんが、thisの挙動を確認するにはいいサンプルです。
push.usage.js
let array = [1,2,3];
let mergeArray=[4,5,6];
let otherArray=[7,8,9];
array.push.apply(array,mergeArray);//[1,2,3,4,5,6]
array.push.apply(otherArray,mergeArray);//array = [1,2,3] oherArray=[7,8,9,4,5,6]
アロー関数
もう一つの例外的な存在がアロー関数です。アロー関数内におけるthisは次のように説明されています。
this はそれを囲むレキシカルコンテキストの this の値が設定されます。グローバルコードでは、グローバルオブジェクトが設定されます。
レキシカルコンテキストの部分がわかりづらいので、別のページにある解説も引用しておきます。
アロー関数は自身の this を持ちません、つまり関数を取り囲む実行コンテキストの this の値が使われます。
基本的にはアロー関数内はその外側にあるthisの値がそのままになると、とらえていて問題ないと思いますが、次のように、実際に動かしてみると認識と違う箇所もありました。
arrow1.js
//非厳格モードでの実行を前提としています
var param = "window";
//コンストラクタ関数
function Person() {
this.param= "person";
//通常通り定義するとthisは呼び出し元になります。
this.showThis0= function(version) {
console.log("showThis0"+version+" is "+this.param);
};
//関数はsetTimeout内で定義されるのでthisの値は変わります。
setTimeout(function showThis1() {
console.log("showThis1 is "+this.param);
},1000);//window
//thisの値をbindにより固定します
setTimeout((function showThis2() {
console.log("showThis2 is "+ this.param);
}).bind(this), 1000);//person (bindの効果)
//これは従来からの記法です。
//thisの値が変化する前にselfに代入して使います。
var self = this;
setTimeout(function showThis3() {
console.log("showThis3 is "+ self.param);
},1000);//person (thisを固定する別の方法)
//アロー関数は、thisの値を変更しません。
//これらの定義時thisはPersonで、それが維持されます。
setTimeout(()=> {
console.log("showThis4 is "+this.param);
},1000);//person (アロー関数)
}
var p = new Person();
//普通に書くとthisは呼び出し元に依存します。
p.showThis0("");//person
//例えばこうするとthisはwindowになります。
let showThis0v2 = p.showThis0;
showThis0v2("v2");//window
arrow2.js
function Person2() {
this.param="person2";
//ここでアロー関数を記述するとPerson2のコンストラクタが定義するので
//thisの値は常にPerson2になります。
this.showThis5=(version)=>{console.log("showThis5"+version+" is "+this.param);};
}
var p2 = new Person2();
p2.showThis5("");//person2
var showThis5v2 = p2.showThis5;
showThis5v2("v2");//person2 アロー関数のthisは呼び出し元に依存ししません。
筆者だけかもしれませんが、オブジェクトとして定義しようとした際は勘違いが起きやすいと思います。次のコードの{}で囲まれた部分はオブジェクトの中で書かれていません。グローバル領域で記述してコンストラクタに渡しています。そのためアロー関数でthisはwindowになります。
arrow3.js
var obj = new Object({
param: "object",
showThis6: ()=> {
console.log("showThis6 is "+this.param);
},
});
//showThis6:()=>{}を定義するのはwindow
obj.showThis6();//window
arrow4.js
function Person3() {
this.param="person3";
//アロー関数の外側にそれを返す関数を定義すると、ふたたび呼び出し元に影響されるようになります。
this.showThis7=function(version){return (()=>{console.log("showThis7"+version+" is "+this.param);})};
}
var p3 = new Person3();
//この時アロー関数を作成するshowThis7のthisがアロー関数のthisに設定されます。
//objからshowThis7を呼び出しているのでthisはPerson3になりアロー関数に引き継がれます。
p3.showThis7("")();//person3;
let showThis7v2=p3.showThis7("v2");
showThis7v2();//person3 一旦生成されたアロー関数は以後はthisの値を変えないのでこの呼び出し方でもpersion3が返ります。
//こうするとアロー関数生成時の呼び出し元はwindowになるのでthisはwindowになります。
let showThis7v3=p3.showThis7;
showThis7v3("v3")();//window
また、アロー関数ではbindやcall、applyでthisを設定しても無視されます。