Edited at

【JS】ああthisよ。君は今、どのオブジェクトなのか(練習問題あり)


はじめに

タイトルはポエム風に書いたが実際、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() // widow(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); // widow
return function () {
console.log(this); // widow
const b = {
c: function() {
console.log(this) // b
return function() {
console.log(this) // widow
}
}
}
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を表示する