JavaScript
ECMAScript
スコープ
this

お前らのJavaScriptのthisの分類は間違っている ~ thisの振る舞いとスコープ ~

 ↑

タイトルで煽る場合はサブタイトルを入れて内容が分かるようにしませんか、という提案


はじめに

古き悪しきECMAScript5の時代には、this


  • 関数呼び出しパターン

  • メソッド呼び出しパターン

  • コンストラクタ呼び出しパターン


  • apply/bind/callパターン

の4つに分類されていました。

ところがECMAScript2015でアロー関数が導入されると、それ以前のthisとは違う、新しいセマンティクス(意味論)が持ち込まれることになりました。

そこで、人々は上の4つの分類に、アロー関数のパターンを付け足して、thisを5つに分類することにしました。

でも、ちょっと待ってください。上の4つのthisとアロー関数のthisの間に決定的な違いがあることに気づいていますか?そもそも、JavaScriptのthisとスコープについて、きちんと理解できていますか?

というわけで、JavaScriptのスコープとthisの振る舞いについて解説した後、正しいthisの分類方法を示したいと思います。タイトルはちょっと過激ですが、真面目なことを書いているので、是非最後まで読んでください。コメントも歓迎です。


2種類のスコープ

プログラミング言語において、スコープには大きく分けて2種類あります。それは静的スコープ動的スコープです。一般的に、振る舞いの分かりやすさから、静的スコープが多くの言語で採用されています。

JavaScript風の疑似コードで、これらのスコープの違いを説明してみます。


静的スコープ


疑似コード

function caller() {

var x = 0;
callee(); // 1
}
var x = 1;
function callee() {
print(x);
}
callee(); // 1

静的スコープの場合、関数callerから関数calleeを呼び出したとき、callee内の変数xが何を指しているかを、以下のような手順を繰り返して決定します。


  • もし現在のスコープでxが宣言されているなら



    • x はそのスコープの変数



  • そうでなければ



    • ソースコード上で1つ外側のスコープを調べる



したがって、関数calleeは常にグローバルなxを参照します。呼び出された関数calleeから呼び出し元の関数caller内の変数を参照することはできません。

構文を解析する段階で変数名を解決できるので、静的スコープと呼ばれています。


動的スコープ


疑似コード

function caller() {

var x = 0;
callee(); // 0
}
var x = 1;
function callee() {
print(x);
}
callee(); // 1

一方で、動的スコープの場合、変数の名前は以下のように解決されます。


  • もし現在のスコープでxが宣言されているなら



    • x はそのスコープの変数



  • そうでなければ



    • 実行のコンテキスト(文脈)において1つ外側のスコープを調べる



そのため、関数callerからcalleeを呼び出した場合は、呼び出し元caller内のxが参照されますが、グローバルスコープでcalleeを呼び出した場合、グローバルな変数xが参照されます。関数を呼び出し元で展開したような振る舞いをします。

実行時の状況によってどの変数を示すかが変わってくるため、動的スコープと呼ばれています。


JavaScriptの変数のスコープ

JavaScriptでは、変数は言わずもがな静的スコープです。varで宣言した変数は関数レベルのスコープで、let/constで宣言した変数はブロックレベルのスコープで、静的に名前が解決されます。


JavaScriptのthisのスコープ

さて、ここからが本題です。

JavaScriptのthisには、動的スコープのthisと静的スコープのthisの2種類があります。つまり、JavaScriptはセマンティクスの根本的に異なる2種類のthisを持つということです。

JavaScriptにおいて、動的スコープを持つ名前はthis以外にもargumentsnew.targetがありますが、静的スコープと動的スコープの2種類があるのはthisだけです。


動的スコープのthis

関数の外部とfunctionキーワードを用いた関数(非同期関数・ジェネレータ関数を含む)内のthisは動的スコープになります。実行のコンテキストによって動的にthisの参照が解決されます。このthisの決定のパターンとして、最初に挙げた4種類があります。


関数呼び出しパターン

thisの解決にあたって、thisを決定する要素を探してスコープを遡ると、グローバルスコープに辿り着きます。

そのため、thisはグローバルスコープにおけるthis(strictモードの場合はundefined、そうでなければグローバルオブジェクト)として解決されます。


メソッド呼び出しパターン

object.method(...)という特殊形式で呼び出した場合、method内のthisの参照はobjectに解決されます。


コンストラクタ呼び出しパターン

new Constructor(...)という特殊形式で呼び出した場合、Constructor内のthisの参照はConstructor.prototypeをクローンして作った新しいオブジェクトに設定されます。


apply/bind/callパターン

これらはFunctionオブジェクトのメソッドですが、リフレクションのような機能を担っています。これらのメソッドは、本来のthisの解決の過程に割り込んで、引数にとったオブジェクトを優先的にthisに割り当てます。


静的スコープのthis

アロー関数(非同期関数も含む)においては、thisは静的スコープを持ちます。すなわち、thisは関数が定義された時点で、そのスコープにおけるthisに静的に解決されます。つまり、thisが他の変数と同様に扱われるということです。

thisが静的に解決されるため、アロー関数はメソッド呼び出しの形式を使ったり、apply/bind/callを使ったりしても、thisを動的に上書きすることはできません。

同じthisというキーワードが使われていますが、アロー関数のthisは他のコンテキストの動的に決定されるthisとは本質的に異なります。


結論

JavaScriptのthisを、単純に5種類に分類するのはナンセンスです。

thisの挙動に基づいて、2段階で分類するべきです。

まず、スコープで分類すると


  • 関数の外部・function関数内の動的スコープのthis

  • アロー関数内の静的スコープのthis

の2種類があります。

さらに、動的スコープのthisについては、thisの解決のパターンとして、


  • 関数呼び出しパターン

  • メソッド呼び出しパターン

  • コンストラクタ呼び出しパターン


  • apply/bind/callパターン

の4つがあります。

したがって、これが正しいthisの分類です。


  • 関数の外部・function関数内の動的スコープのthis


    • 関数呼び出しパターン

    • メソッド呼び出しパターン

    • コンストラクタ呼び出しパターン


    • apply/bind/callパターン



  • アロー関数内の静的スコープのthis

動的スコープ・静的スコープを意識すると、thisの振る舞いが理解しやすいのではないでしょうか。