はじめに
タイトルはポエム風に書いたが実際、thisが何を指すのか分からなくなることがよくある。
JavaScriptのthisとは何なのか、何を指すのか、この度色々実験して分かったことがあるので、自分の中での整理も兼ねて記事にしておこうと思う。
対象読者: JSのthisがいまいち分かっていない方〜なんとなく使っているがthisが何を指しているのかよく迷う方
nodeでなく、ブラウザでの挙動を前提とする。
※ 最後に練習問題も用意したので、腕に自信のある方はそちらを先にどうぞ
thisの性質
単にthis、thisと言ってもそれが何を指すのかあいまいなので、ざっくりここでは以下の性質を持つオブジェクトをthisと考える。(厳密な定義は難しいのでここではしないが、これで十分だと思う)
- JSのコードの任意の場所で、
this
変数でアクセスできるオブジェクト(代入はできない)。 - thisが呼ばれたその状況により変化する(雑)。特に、関数内スコープ以外で変化することはなく、関数の外においては常にwindowである。
- (網羅できているわけではないが以降に説明される形で決定される。)
そもそも、thisがあると何が嬉しいのか
そもそもthisがあると何が嬉しいのか。考えたが、やはりthisがあると便利ということなんだと思う。
関数はthisに関連する操作を行いたいことが多いので、暗黙的に利用できるオブジェクトが用意されているのは便利。これがないと、thisに相当するオブジェクトを引数で渡さないといけない(逆にその手間が許容できるならthisなどというものは使わなくて良い、はず)
receiver
深く関連するワードとして、receiverと呼ばれる概念、オブジェクトがある。これの定義は次の項目を見ていただくとして、基本的にthisはreceiverオブジェクトを指す。つまりreceiverが何なのか分かれば、thisもすぐ分かる。というわけで、receiverが何にどうやって決まるのかをこの後は見ていく。難しくない。
JavaScriptのreceiverとは
厳密な定義は違うだろうが、僕なりにJSのreceiverは次のようなものであると考えた。
〜定義〜
B()の関数呼び出しに対するreceiverは次のようにして得られるAである。
-
A.B()
の形にできるときの、オブジェクトA。このときBは必ずAのプロパティとなっている -
A.B()
の形にできないとき、つまりB()
のように単体で呼んでいるときはA=windowとする。
簡単ですね。
receiverの具体例
さっそく具体例を見る。
A.B() => 関数呼び出しのreceiverは、A
B() => 関数呼び出しのreceiverは、window
これを使って考えていける。以下、Aをreceiver、Bを呼び出している関数とする。
X.Y.Z()の場合。
Z()の関数呼び出しは、「A=X.Y、B=Z」
X.Y(1)(2)の場合。
引数1の関数呼び出しは、「A=X、B=Y」
引数2の関数呼び出しは、「A=window、B=X.Y(1)」
C(1)(2).D(3)(4)の場合。(練習問題4を参照)
引数1の関数呼び出しは、「A=window、B=C」
引数2の関数呼び出しは、「A=window、B=C(1)」
引数3の関数呼び出しは、「A=C(1)(2)、B=D」
引数4の関数呼び出しは、「A=window、B=C(1)(2).D(3)」
receiverという名前について
ところで例でのAはどちらかというとBを呼び出している主体に見えるので、receiverという呼び方は不適切なのではないか(caller?sender?などの方が正しいのではないか)という風に思ってしまいがちだが、そうではない。
オブジェクト指向の考え方ではこの場合、親オブジェクト(この場合、window)がAオブジェクトにBというメッセージを送っていると考える。このときのメッセージを改めて書くと、
- 送り主(sender): この関数を呼び出したオブジェクト(window)
- 送り先(receiver):
A
オブジェクト - メッセージの内容(message):
B(引数があれば引数も)
なのでAがreceiverとなる。(receiver, sender, messageなどはオブジェクト指向という見方からコードを見たとき限定の言葉の選び方な気がするので、JavaScriptをOOP以外の見方で見る場合には別のワードで表現しないといけない気もするが、深入りできないのでしない。)
thisの挙動を見ていく
receiverが何であるかをただ追えば良い。
const obj = {}
obj.func = function() { console.log(this) } // objのメソッドとしてfunc関数を定義
/*
あるいは、
const obj = {
func: function() { console.log(this) }
}
*/
obj.func() // obj(A.B()の形。receiverがobjなので)
const context = {}
context.func = obj.func
context.func() // context(A.B()の形。receiverがcontextなので。)
const assingedToGlobalVar = obj.func
assingedToGlobalVar() // window(B()の形。receiverがwindow。同じ理由。以下bindの項までthis=receiverである)
B()の形の場合receiverはwindowとなる。呼び出し時におけるthisは関係ない。(threeFuncs内部のfunc()がwindowを表示している)
const func = function() { console.log(this) } // window下の変数としてfunc関数を定義
func() // window
const context = {}
const subContext = { func }
context.threeFuncs = function() {
console.log(this) // context
subContext.func() // subContext
func() // window
}
context.threeFuncs()
これを利用すれば、任意の関数
を 任意のオブジェクト(objとする)にプロパティとして代入し、objをreceiverとして呼ぶ
ことにより、thisをobjに変えて実行できる
(変えることができてしまう)
もちろんそういった用途で関数を用意し、利用時にthisを決めたいことも多い。ただし、そうではなくthisをあらかじめ固定しておきたいときもある。そういったときは、Function.prototype.bind
を利用する。bindは、thisが指すオブジェクトを固定することができる。
具体的には、その関数内のthisを、bindの引数オブジェクト固定したfunctionを新たに生成して返す。
const func = function() { console.log(this) }
const context = {}
const subContext = {}
// bindした結果の関数をcontextのpropertyに指定する
context.contextIsWindowFunc = func.bind(this) // グローバルでのthisはwindow
context.contextIsSubContextFunc = func.bind(subContext) // thisをsubContextに固定
context.contextIsContextFunc = func
// contextにpropertyとして指定した関数たちを呼ぶ
context.contextIsWindowFunc() // window(receiverはcontext。ここに来てreceiver != thisになった)
context.contextIsSubContextFunc() // subContext(receiverはcontextなので、receiver != this)
context.contextIsContextFunc() // context
また、ES6で導入されたアロー関数は暗黙的に、関数が定義されたスコープにおけるthisにbindした関数を生成する。
// グローバルなスコープでのthisは必ずwindowなので、thisはwindowに固定
const func = () => {
console.log(this)
}
const context = { func }
context.func() // window
const context = {
func: function() {
// 実行時にこのスコープのthisは決まるので、ここで定義しているarrow関数のthisも実行時に決まる
const f = () => { console.log(this) }
f()
}
}
context.func() // context
subContext = {}
const func = context.func
func() // window
まとめ
thisが何を指すのか分からなくなりがちだが、bindなどされていない場合にはreceiverと考えて良い。
なので、receiverの定義にしたがってreceiverを見つけ出す。上に書いた定義をよく読む(大事)
bindを利用したときはbindの引数オブジェクトにthisが固定される(固定された関数が新たに作られる)。
アロー関数は便利だが、暗黙的にbindしているので注意して使う。
ここでは紹介しなかったが、applyなど呼び出し時にthisオブジェクトを指定して関数を呼び出すこともできるが、この時の挙動はあらかじめbindしていた場合と同じように考えれば問題ない。
良ければいいねをお願いします。
また、追加の練習問題と回答も募集しています。コメント欄より教えてください。
練習問題
練習問題を用意したので、ログに吐かれるthisがどれを指すのか当ててください。回答、解説は下にあります。
問題1.
const obj = {
func: function() {
const f = function(){ console.log(this) }
console.log(this)
f()
}
}
obj.func()
問題2.
const a = {
b: {
c: this
}
}
console.log(a.b.c)
問題3.
const a = {
b: function (){
return {
c: {
d: this
}
}
}
}
console.log(a.b().c.d)
問題4.
const a = function() {
console.log(this);
return function () {
console.log(this);
const b = {
c: function() {
console.log(this)
return function() {
console.log(this)
}
}
}
return b
};
};
a()().c()()
問題5.
const func = function() {
return function () {
const c = {
a: function () {
console.log(this);
},
b: () => {
console.log(this);
}
};
return c
};
};
func()().a();
func()().b();
ここから回答
練習問題の回答
問題1.(回答)
const obj = {
func: function() {
const f = function(){ console.log(this) }
console.log(this) // obj
f() // window
}
}
obj.func()
〜解説〜
obj.func()なので、func()内でのthisはreceiverであるobj
しかし、f()なので、f()内でのthisはwindow
問題2.(回答)
const a = {
b: {
c: this
}
}
console.log(a.b.c) // window
〜解説〜
オブジェクトリテラルにおいて、valueは再帰的に即時評価される
その間、関数呼び出しは発生しないのでthisは呼び出したタイミングでのthis、すなわちwindow
問題3.(回答)
const a = {
b: function (){
return {
c: {
d: this
}
}
}
}
console.log(a.b().c.d) // a
〜解説〜
a.b().c.dなので、b()内でのthisはreceiverであるa
オブジェクトリテラルにおいて、valueは再帰的に即時評価されるのでthisはa
問題4.(回答)
const a = function() {
console.log(this); // window
return function () {
console.log(this); // window
const b = {
c: function() {
console.log(this) // b
return function() {
console.log(this) // window
}
}
}
return b
};
};
a()().c()()
〜解説〜
Receiverの定義の具体例を参照
問題5.(回答)
const func = function() {
return function () {
const c = {
a: function () {
console.log(this);
},
b: () => {
console.log(this);
}
};
return c
};
};
func()().a(); // c
func()().b(); // window
〜解説〜
func()()はcオブジェクトを返す。
c.a()になるので、aはcを表示する。
cオブジェクトが初期化されたタイミングはfunc()()のタイミング、つまりA=window、B=func()のタイミングなので、this=windowである。
このとき関数bはwindowにbindされるため、c.b()はwindowを表示する