はじめに
さっき久しぶりにthisを失う事故に遭いました。
そもそもメンバ関数を直接他所に渡す、という事をあまりする機会がないので、そこでthisが失われる経験を定期的にやりつつも、しばらく経つとすぐに原因を忘れています。
しかしいい加減、なんでそれがダメなのかを覚えないと同じミスをするなと思い、最近のぼくの話相手であるChatGPTに色々と聞いてみました。
なので今回の記事はほぼノーソース(ChatGPTに聞いただけ)という事を前置きしておきます。
thisを失うとは
javascriptはその言語仕様上、『基本的に』thisはその関数がドット演算子で結ばれた呼び出し元のオブジェクトを指します。
なので、ドット演算子を失った(つまり関数だけが別の場所に委譲された)場合、thisはundefined(strictモードでなければグローバルスコープ)になり、委譲された関数がthisを参照しようとすると参照先がない、となります。
なぜこんな仕様なのか
そもそも歴史的にclassやbindという概念がjavascript登場時になく、しかしthisは当初から存在していた事が原因、といえば原因です。
実際にこの仕様を策定した背景まで調べられていませんが「その関数を特に作り替えることなく、文脈を切り替えて関数を使いまわせたら便利だな」という思いは想像しやすいです。きっと当時もそういう思いでthisという機能は実装されたのだと思います。(thisの抱えている意味合いがほとんどparentだろ、というツッコミは置いておきつつ)
なので「何故こんな仕様にしたのか」に対しては「当時の仕様がここから始まったから」という煙に巻いたような回答が正しいと思います。
仕様の変遷
いまにして思えばどうしてこうなったという仕様に感じますが、当時はこの仕様で特に問題はなかったはずです。何故なら、仕様の適用条件は『基本的に』ではなく、『必ず』だったからです。
この『基本的に』という前置きが必要になるのは、bind(ECMAScript 5 2009年)やアロー関数(ECMAScript 6 2015年)が登場したからです。
補足しておくと、bindはnewFunc = bind(func, obj)と書くと、newFuncを呼ぶ限り、func内のthisが必ずobjになるというもので、アロー関数はfunction() {return () => {}}と書くと内側のアロー関数は『必ず』functionのthisを保持し続ける(thisの更新が行われない)というものです。
では何故、これらが生まれる事になったのか?
詳細な歴史は調べていませんが、class実装前にはbindで疑似的なclass実装の記事が流行っていた事や、アロー関数と同時にclassが実装された事を踏まえると、長い間class機能が求められていたのだろうというのは当時の雰囲気から感じます。
趣味でコーディングしてるだけの自分ですら昔はjsってclass機能ないのかよって思ってました。
後付けの機能の方が重宝される不幸
自分はたぶん2015年頃くらいに初めてjavascriptを触ったのですが、当時はclassが(ギリギリなかったか、実装直後で資料を見つけられ)なかったので、thisを触る機会がほぼありませんでした。
bindでclassっぽいものが作れる、という記事はいくつか見たのですが、初学者に「昔ながらのthis仕様」と「例外が加わったthis仕様」を理解するのは難しく、またbindという考え方自体C++でクラスを初めてさわったぼくにとっては理解しがたいものでした。(そもそも当時javascriptを触っていたのは、単にhtmlのちょっとした加工が目的だったので、そこまで深くjsの仕様に立ち入る必要がなかった)
時は流れ、久しぶりにjavascriptを触ってみたらclassが追加されていびっくりしつつ、thisが普通のclassと同じ感じに使えることに感動しました。
classを使えるようになってからはjavascriptをローカルで走らせることも多くなりました。
そしてbindという昔ながらの(疑似)classの作り方があったことも忘れ、なぜそんな技術が必要だったのか、という事を考える機会もないまま今日をむかえました。
この思考停止なclass使用が結果として冒頭の不幸を招くこととなりました。
classに先立ってbindが実装された理由
これは思うにclassより安価に実装(大筋は既にあるcallやapplyをwrapするような関数をつくれば)できて、bindを使うことでclassらしい振る舞いを(ちょっと手間をかければ)できる事が理由だと思います。
bindで作られた疑似classの情報を基に正規版classとして求められる要件を洗い出しつつ・・みたいな流れがあったのかなと勝手に想像しています。
bind時点ではあくまでcallやapplyの延長のため、この時点で疑似classを扱っていた人たちにとってこの思想を読み解くことはそこまで難しくなかったと思います。
おわりに
とりとめのない記事になりましたが、こうして書いてみると今の不愉快な仕様に対して寛容になれる気持ちと、クソがという相反する気持ちがそこそこでてきます。
javascriptは関数が第一市民の言語で、登場当初はオブジェクト指向的なクラス構文は存在しませんでした。
関数を柔軟に扱えることを重視した仕様だったため、同じ関数を別のコンテキストで再利用するためのthis設計には、ある種の“緩さ”があったとも言えます。
それが「クラス」や「スコープ」が求められる時代に突入し、必要な継ぎ足しとして bind や class が導入された。
求めに応じて継ぎ足し継ぎ足しした故に、不整合・不条理が残る形になったという背景を正しく知ることで、今のエラーと向き合えるようにしたいと思いました。
補足
thisがスコープを失う理由は先に書いた通りなので、例えばアロー関数を使えばthisを失わないメンバ関数を定義できます。
class A {
get methodA() {
return () => {
// ここに書かれたthisは必ず、Aインスタンス
// ただしこの書き方だと毎度関数インスタンスを生成することになる
};
}
// インスタンスの再生成が気に食わなければキャッシュを使えばOK
#methodB;
get methodB() {
if(this.#methodB === undefined) {
this.#methodB = () => {
};
}
return this.#methodB;
}
}