Closed28

【JavaScript】thisの挙動メモ

nanasinanasi

そもそもthisとは?

thisは自身がどのオブジェクトから呼び出されたかを記録する変数
なのでどんな場合でもオブジェクトになる
(例外: グローバルでstrictモードの場合: undefined

それを考えると、呼び出し場所によって変わるのは当たり前に思えてくる

nanasinanasi

例えば、counterオブジェクトを考えてみる

const counter = {
    count: 0,
    add() { // これはfunction
        return ++this.count
    }
}
nanasinanasi

これをcounter.add()と呼び出した場合、counter.addに入っている関数はcounterオブジェクトから呼び出されている

そのためthiscounterとなり、結果カウンターは正常に動く

nanasinanasi

関数がどのオブジェクトにも属さず実行された場合、thisはブラウザの場合windowになる

const add = counter.add
add() // thisはwindow

そもそもcounter.addに入っているのはただの関数なので、言語仕様的に単独で実行できる
このadd関数自体はcounterオブジェクトの情報を持っていない

と考えると、thiscounterになるわけない気がしてくる

nanasinanasi

これを使うと、メソッド集め的な=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
nanasinanasi

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

nanasinanasi

ミックスインじゃないけど、プロトタイプに入れればクラスにもできる

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が何を指すのかは、なんか自身のオブジェクトを指すくらいしかわからない

nanasinanasi

thisを固定する

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

nanasinanasi

例えばここにお馴染みのカウンターがある

const counter = {
    count: 0,
    add() { // これはfunction
        return ++this.count
    }
}

このaddはどのオブジェクトからでもなくグローバルから呼び出しているので、thiswindowになる

const add = counter.add
add() // NaN
nanasinanasi

と思ったけど、これもしかしてwindow.addと解釈されてる?
それなら確かにaddthiswindowになる

そして、この挙動がややこしいから、strictモードなら後付けでundefinedになるようになった?

nanasinanasi

bindは引数にthisにしたいオブジェクトを取る
bindバインド済み関数を返す

バインド済み関数

簡単に言うと、作成以降はthisが変動しない関数
本来thisに使われるはずだった、どこから呼び出されたかのオブジェクトは見なかったことになる

MDNのこの文が割と刺さる

初心者の JavaScript プログラマーがよくやる間違いは、あるオブジェクトからメソッドを抽出し、後でその関数を呼び出すとき、その内側の this 値が元のオブジェクトになると考えてしまうことです(例えば、そのメソッドをコールバック関数に使う場合)。

特に配慮しなければ、ふつうは元のオブジェクトが見えなくなります。

nanasinanasi

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')
nanasinanasi

別のオブジェクトに関数を入れる場合の違い

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
nanasinanasi

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

nanasinanasi

アロー関数の場合

アロー関数はここまでの法則に一切当てはまらない変種
アロー関数は作成された時点でのthisを保持する

作成された時点でのthisとは、アロー関数の作成を書いた関数でのthis

オブジェクトの中にアロー関数を書いても、そのオブジェクトが作成されたのがグローバルなら、thisはグローバルに固定される

// ここグローバルで、どの関数の中にもない
const obj = {
  func: () => this // thisはグローバル=windowに固定される
}
obj.func() // window

const obj2 = { func: obj.func }
obj2.func() // windowに固定されてるのでwindowになる

というかそもそもthis自体、関数をどのオブジェクトに書いたかはあまり関係ない気がする
問題は関数の呼び出し場所か、アロー関数ならどの関数で作成されたかを気にすべきかも

nanasinanasi

アロー関数のthis

作成された場所
=どの関数の中で作成されたか
=作成された場所(関数)のthisは何か
=作成された場所(関数)はどのオブジェクトから呼び出されたか

みたいなものに影響を受けそう

nanasinanasi

関数内でアロー関数を作成する

こういうの

function func() {
    const allow = () => this // このthisは?
}

これがまた結構ややこしい

nanasinanasi

クロージャ

関数からアロー関数を返す場合、そのアロー関数の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になる
nanasinanasi

呼び出すだけ

この場合アロー関数が作成されるのは、funcが呼び出されたタイミング
もっと言うと、allowfuncが呼び出されるたびに毎回作り直される

const obj = {
    func() {
        // ここのthisはfuncが呼び出された場所による
        const allow = () => this // funcが呼び出されたときのthisになる
        return allow()
    }
}

そのため、allowthisがどうなるかはわからない
funcがどこから呼び出されたかによる
これだと正直アロー関数感ない

obj.func() // obj
const func = obj.func
func() // window
nanasinanasi

しかし、関数の中で関数を宣言するとまた事情は変わる

const obj = {
    func() {
        const func2 = function() {return this}
        return func2()
    }
}

obj.func() // window

このfunc2はどのオブジェクトからも呼び出されていない
そのため、なんだか直感的ではないけど、obj.func()windowになる

また、func2の呼び出しはfuncから行われているので、objの外から干渉することはできない
そのためある意味func2thisは固定されている

こんなふうに呼び出し方で結果が変わる関数を、どう呼び出すかを決めてしまうことで結果を固定できるみたいなことができる
やってることはそんなにむずくなさそう

nanasinanasi

ちなみに、1行挟むだけで別の結果に固定できる
これはfunc2obj2から呼び出せるようにした例

const obj = {
    func() {
        const func2 = function() {return this}
        const obj2 = { func2 }
        return obj2.func2()
    }
}

obj.func() // obj2
nanasinanasi

おまけ: クラスのメソッド内で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
  }
}
junerjuner

むしろそれは こうしがちがある。

class Example {
  value = 10;
  #getValue() {
    return this.value;
  } 
  method() {
    console.log(this.#getValue()) // 10
  }
}
nanasinanasi

まとめ

  • thisはこの関数がどこから呼び出されたかを知れる変数
    • 「どこ」の区切りはオブジェクト
    • 例: obj.func()funcobjから呼び出されている
  • bindを使うとバインド済み関数=thisが固定された関数が作れる
  • アロー関数のthisは作成時点から変わらない
    • 作成時点: 「どの関数で作成されたか」=「作成された場所でのthisはなんだったのか」
    • グローバルで作成した場合、作成時点のthiswindow=アロー関数のthiswindowで固定
  • 関数の中でアロー関数を作れる
Hidden comment
このスクラップは2024/12/05にクローズされました