this は、書いた場所ではなく呼ばれ方で決まるからです。
obj.method() と書けば this は obj になります。
関数を取り出して fn() と呼べば、obj はもうどこにもありません。
関数は自分がどこに書かれていたかを覚えていません。
同じ関数です。 呼び方だけを変えたら、結果が変わりました。
何が渡されているのか
user.greet() という式は、2つのことを同時にしています。
userからgreetという関数を取り出すuserをthisとして渡しながら呼ぶ
const fn = user.greet は1だけをします。user は捨てられます。
だから fn() には渡すものが無くなります。
| 呼び方 | this |
|---|---|
user.greet() |
user |
fn() |
無い(下で説明) |
greet.call(other) |
other |
new Thing() |
新しく作られたもの |
「無い」ときに何になるか
ここが分かりにくいところです。モードによって違います。
渡すものが無いと、グローバルオブジェクトが入ります。
globalThis.name は無いので、最初の例では undefined が出ていました。
'use strict' を付けると、代わりに undefined が入ります。
こちらのほうが親切です。 次に this.name を読もうとした時点で落ちるからです。
forEach() に渡した関数は、counter とは無関係に呼ばれます。
メソッドの中に書いてあっても、this は引き継がれません。
import/exportのあるファイル(モジュール)は、書かなくても strict モードです。 実務では下の例外になるほうが普通です。
→ Cannot read properties of undefined
アロー関数だけが違う
アロー関数は、自分の this を持ちません。
呼ばれ方に関係なく、書かれた場所の this をそのまま使います。
addAll が counter.addAll() として呼ばれているので、そこでの this は counter です。
アロー関数はそれをそのまま借ります。
「呼び方で決まる」という規則の唯一の例外がこれです。
だからメソッドには使わない
arrow が書かれているのはオブジェクトリテラルの中で、そこはまだ user の中ではありません。
借りてくるのは外側の this です。user にはなりません。
| 使う場所 | 選ぶもの |
|---|---|
| オブジェクトのメソッド | method() {} |
| メソッドの中のコールバック | アロー関数 |
| クラスのフィールドに置く関数 | アロー関数(this が固定される) |
自分で渡す
call() / apply() / bind() で、this を指定できます。
bind() で作った関数は、あとから call() しても変わりません。
一度結んだら外れないので、コールバックとして渡すときに使えます。
よくある誤解
「メソッドの中に書けば this はそのオブジェクト」
違います。書いた場所は関係ありません。
上の forEach の例がそれで、メソッドの中に書いてあっても外れます。
「アロー関数は this を親から受け取る」
正確には、受け取っていません。そもそも持っていないので、
this と書かれたら外側のスコープを見に行くだけです。
だから call() で渡しても効きません。
{ name: 'い' } は無視されました。
「this を使わなければ安全」
そのとおりです。渡さなくて済む形にできるなら、そのほうが単純です。
引数で受け取れば、呼び方に左右されません。
自分で確かめる
上のコードは書き換えて実行できます。次を試すと違いが見えます。
'use strict'の行を消すと、undefinedがglobalThisに変わるforEach((x) => ...)をforEach(function (x) { ... })に戻すと落ちるbind(other)のあとに.call(user)しても変わらない
まとめ
thisは呼ばれた瞬間に決まる。書いた場所は関係ないobj.method()ならobj、取り出してfn()なら渡すものが無い- 渡すものが無いとき、sloppy では
globalThis、strict ではundefined - アロー関数は
thisを持たない。外側のものをそのまま使う - メソッドはふつうの書き方、その中のコールバックはアロー関数
コールバックにアロー関数を渡す形は、非同期処理をまとめて走らせるときにもよく出ます。 → 複数の非同期処理を同時に走らせる