【JavaScript】thisの挙動メモ

this
とは?
そもそもthis
は自身がどのオブジェクトから呼び出されたかを記録する変数
なのでどんな場合でもオブジェクトになる
(例外: グローバルでstrict
モードの場合: undefined
)
それを考えると、呼び出し場所によって変わるのは当たり前に思えてくる

例えば、counter
オブジェクトを考えてみる
const counter = {
count: 0,
add() { // これはfunction
return ++this.count
}
}

これをcounter.add()
と呼び出した場合、counter.add
に入っている関数はcounter
オブジェクトから呼び出されている
そのためthis
はcounter
となり、結果カウンターは正常に動く

関数がどのオブジェクトにも属さず実行された場合、this
はブラウザの場合window
になる
const add = counter.add
add() // thisはwindow
そもそもcounter.add
に入っているのはただの関数なので、言語仕様的に単独で実行できる
このadd
関数自体はcounter
オブジェクトの情報を持っていない
と考えると、this
がcounter
になるわけない気がしてくる

これを使うと、メソッド集め的な=Scalaのtrait
的なものが作れるかもしれない
なお、クラスにミックスインはできず、Scalaでいうobject
にミックスインするみたいな感じ
まず、これがトレイト
このメソッドは直接呼び出してはいけない
const counterTrait = {
increment() {
return this._count++
},
decrement() {
return this._count--
},
get count() {
return this._count
}
}
// NG: こうやって使うものではないし動かない
counterTrait.increment()
そしてこれをミックスイン(実装とも言う)する
このトレイトもどきは_count
プロパティが必要なので、これを初期化しておく
_count
はScalaだったらprotected
にしてたと思う
const counterImpl = {
_count: 0,
...counterTrait
}
console.log(counterImpl.increment()) // 0
console.log(counterImpl.increment()) // 1
console.log(counterImpl.increment()) // 2

便利と言えば便利かもしれないけど、正直いってややこしいから使いたくはない

ミックスインじゃないけど、プロトタイプに入れればクラスにもできる
const CounterImpl = function () {
this._count = 0
}
CounterImpl.prototype = counterTrait
const counter = new CounterImpl()
console.log(counter.increment()) // 0
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
プロトタイプにしたオブジェクトのthis
が何を指すのかは、なんか自身のオブジェクトを指すくらいしかわからない

this
を固定する
とはいえこれだと不便だということで、特定の関数に対してthis
を固定する方法が用意されてる
それがbind
メソッド

例えばここにお馴染みのカウンターがある
const counter = {
count: 0,
add() { // これはfunction
return ++this.count
}
}
このadd
はどのオブジェクトからでもなくグローバルから呼び出しているので、this
はwindow
になる
const add = counter.add
add() // NaN

と思ったけど、これもしかしてwindow.add
と解釈されてる?
それなら確かにadd
のthis
はwindow
になる
そして、この挙動がややこしいから、strict
モードなら後付けでundefined
になるようになった?

bind
は引数にthis
にしたいオブジェクトを取る
bind
はバインド済み関数を返す
バインド済み関数
簡単に言うと、作成以降はthis
が変動しない関数
本来this
に使われるはずだった、どこから呼び出されたかのオブジェクトは見なかったことになる
MDNのこの文が割と刺さる
初心者の JavaScript プログラマーがよくやる間違いは、あるオブジェクトからメソッドを抽出し、後でその関数を呼び出すとき、その内側の this 値が元のオブジェクトになると考えてしまうことです(例えば、そのメソッドをコールバック関数に使う場合)。
特に配慮しなければ、ふつうは元のオブジェクトが見えなくなります。

例
const add = counter.add.bind(counter) // thisをcounterで固定する
add() // 0
add() // 1
const add = counter.add.bind(null) // thisをnullで固定する
add() // Error: null is not an object (evaluating 'this._count')

別のオブジェクトに関数を入れる場合の違い
bind
あり
const bound = counter.increment.bind(counter)
const wrap = {
_count: 777, // これは参照されない
increment: bound
}
console.log(wrap.increment()) // 0
console.log(wrap.increment()) // 1
bind
なし
const bound = counter.increment
const wrap = {
_count: 777, // これが参照される
increment: bound
}
console.log(wrap.increment()) // 777
console.log(wrap.increment()) // 778

ちなみに、バインド済み関数に対してbind
を呼び出した場合、this
は上書きされない
あとbind
には引数をいい感じに登録しとく効果もある

アロー関数の場合
アロー関数はここまでの法則に一切当てはまらない変種
アロー関数は作成された時点でのthis
を保持する
作成された時点でのthis
とは、アロー関数の作成を書いた関数でのthis
オブジェクトの中にアロー関数を書いても、そのオブジェクトが作成されたのがグローバルなら、this
はグローバルに固定される
// ここグローバルで、どの関数の中にもない
const obj = {
func: () => this // thisはグローバル=windowに固定される
}
obj.func() // window
const obj2 = { func: obj.func }
obj2.func() // windowに固定されてるのでwindowになる
というかそもそもthis
自体、関数をどのオブジェクトに書いたかはあまり関係ない気がする
問題は関数の呼び出し場所か、アロー関数ならどの関数で作成されたかを気にすべきかも

関数内でアロー関数を作成する
こういうの
function func() {
const allow = () => this // このthisは?
}
これがまた結構ややこしい

クロージャ
関数からアロー関数を返す場合、そのアロー関数のthis
はその関数がどこから呼び出されたかに影響を受ける
そして一度作成したアロー関数は、関数の外を出てもthis
が変わることはない
const obj = {
func() {
// ここのthisはfuncが呼び出された場所による
const allow = () => this // funcが呼び出されたときのthisになる
return allow
}
}
const allow = obj.func()
allow() // obj
const func = obj.func
func()() // window
// このfuncはwindowオブジェクトから呼び出しているので、
// funcもallowもthisはwindowになる

呼び出すだけ
この場合アロー関数が作成されるのは、func
が呼び出されたタイミング
もっと言うと、allow
はfunc
が呼び出されるたびに毎回作り直される
const obj = {
func() {
// ここのthisはfuncが呼び出された場所による
const allow = () => this // funcが呼び出されたときのthisになる
return allow()
}
}
そのため、allow
のthis
がどうなるかはわからない
func
がどこから呼び出されたかによる
これだと正直アロー関数感ない
obj.func() // obj
const func = obj.func
func() // window

しかし、関数の中で関数を宣言するとまた事情は変わる
const obj = {
func() {
const func2 = function() {return this}
return func2()
}
}
obj.func() // window
このfunc2
はどのオブジェクトからも呼び出されていない
そのため、なんだか直感的ではないけど、obj.func()
はwindow
になる
また、func2
の呼び出しはfunc
から行われているので、obj
の外から干渉することはできない
そのためある意味func2
のthis
は固定されている
こんなふうに呼び出し方で結果が変わる関数を、どう呼び出すかを決めてしまうことで結果を固定できるみたいなことができる
やってることはそんなにむずくなさそう

ちなみに、1行挟むだけで別の結果に固定できる
これはfunc2
をobj2
から呼び出せるようにした例
const obj = {
func() {
const func2 = function() {return this}
const obj2 = { func2 }
return obj2.func2()
}
}
obj.func() // obj2

おまけ: クラスのメソッド内でfunction
を使うと、基本的にthis
で詰む
class Example {
value = 10
method() {
// OK
const getValue = () => this.value
console.log(getValue()) // 10
// NG
const getValue = function() { return this.value }
console.log(getValue()) // window
// OK
const getValue = function() { return this.value }
console.log(getValue.bind(this)()) // 10
}
}
むしろそれは こうしがちがある。
class Example {
value = 10;
#getValue() {
return this.value;
}
method() {
console.log(this.#getValue()) // 10
}
}

ありがとうございます!

まとめ
-
this
はこの関数がどこから呼び出されたかを知れる変数- 「どこ」の区切りはオブジェクト
- 例:
obj.func()
のfunc
はobj
から呼び出されている
-
bind
を使うとバインド済み関数=this
が固定された関数が作れる - アロー関数の
this
は作成時点から変わらない- 作成時点: 「どの関数で作成されたか」=「作成された場所での
this
はなんだったのか」 - グローバルで作成した場合、作成時点の
this
はwindow
=アロー関数のthis
もwindow
で固定
- 作成時点: 「どの関数で作成されたか」=「作成された場所での
- 関数の中でアロー関数を作れる