お前らのJavaScriptのthisの分類は間違っている ~ thisの振る舞いとスコープ ~
お前らのJavaScriptのthisの分類は間違っている ~ thisの振る舞いとスコープ ~:
↑
タイトルで煽る場合はサブタイトルを入れて内容が分かるようにしませんか、という提案
古き悪しきECMAScript5の時代には、
ところがECMAScript2015でアロー関数が導入されると、それ以前の
そこで、人々は上の4つの分類に、アロー関数のパターンを付け足して、
でも、ちょっと待ってください。上の4つの
というわけで、JavaScriptのスコープと
プログラミング言語において、スコープには大きく分けて2種類あります。それは静的スコープと動的スコープです。一般的に、振る舞いの分かりやすさから、静的スコープが多くの言語で採用されています。
JavaScript風の疑似コードで、これらのスコープの違いを説明してみます。
静的スコープの場合、関数
構文を解析する段階で変数名を解決できるので、静的スコープと呼ばれています。
一方で、動的スコープの場合、変数の名前は以下のように解決されます。
実行時の状況によってどの変数を示すかが変わってくるため、動的スコープと呼ばれています。
JavaScriptでは、変数は言わずもがな静的スコープです。
さて、ここからが本題です。
JavaScriptの
JavaScriptにおいて、動的スコープを持つ名前は
関数の外部と
そのため、
これらは
アロー関数(非同期関数も含む)においては、
同じ
JavaScriptの
まず、スコープで分類すると
さらに、動的スコープの
したがって、これが正しい
↑
タイトルで煽る場合はサブタイトルを入れて内容が分かるようにしませんか、という提案
はじめに
古き悪しきECMAScript5の時代には、thisは- 関数呼び出しパターン
- メソッド呼び出しパターン
- コンストラクタ呼び出しパターン
-
apply/bind/callパターン
ところが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以外にもargumentsやnew.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
さらに、動的スコープの
thisについては、thisの解決のパターンとして、- 関数呼び出しパターン
- メソッド呼び出しパターン
- コンストラクタ呼び出しパターン
-
apply/bind/callパターン
したがって、これが正しい
thisの分類です。- 関数の外部・
function関数内の動的スコープのthis
- 関数呼び出しパターン
- メソッド呼び出しパターン
- コンストラクタ呼び出しパターン
-
apply/bind/callパターン
- アロー関数内の静的スコープの
this
thisの振る舞いが理解しやすいのではないでしょうか。
コメント
コメントを投稿