91
92

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-06-29

はじめに

タイトルはポエム風に書いたが実際、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を表示する

91
92
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
91
92

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?