【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を表示する