クエリ「scope」の画像。 ノスタルジアの攻撃を引き起こした場合は申し訳ありません)
スコープの基本的な定義は次のとおりです。これは、コンパイラが変数と関数を必要なときに検索する領域です。 簡単すぎると思う? 一緒に理解することを申し出ます。
JavaScriptインタープリター
スコープについて話す前に、JavaScriptインタープリターについて議論し、それがさまざまなスコープにどのように影響するかを検討する必要があります。 JSコードを実行するとき、インタープリターは2回それを通過します。
コンパイルパスとも呼ばれるコードの最初のパスは、スコープに最も影響します。 インタープリターは、変数と関数の宣言のコードをスキャンし、これらの宣言を現在のスコープの最上位に上げます。 変数宣言のみが発生し、割り当て操作は実行パスと呼ばれる次のパスのためにそのままであることに注意することが重要です。
これをよりよく理解するために、簡単なコードを考えてみましょう。
'use strict' var foo = 'foo'; var wow = 'wow'; function bar (wow) { var pow = 'pow'; console.log(foo); // 'foo' console.log(wow); // 'zoom' } bar('zoom'); console.log(pow); // ReferenceError: pow is not defined
このコードは、コンパイル後、次のようになります。
'use strict' // var foo; var wow; // , , function bar (wow) { var pow; pow = 'pow'; console.log(foo); console.log(wow); } foo = 'foo'; wow = 'wow'; bar('zoom'); console.log(pow); // ReferenceError: pow is not defined
ここでは、広告が現在のスコープの最上位にあるという事実に注意を払う必要があります。 これは、以下に示すように、JavaScriptの範囲を理解するために非常に重要です。
たとえば、
pow
変数は
bar
関数で宣言されてい
bar
が、これはスコープであるためです。 関数、スコープに関連して、変数が親で宣言されていないことに注意してください。
bar
関数の
wow
パラメーターも関数のスコープで宣言されます。 実際、関数のすべてのパラメーターはそのスコープ内で暗黙的に宣言されているため、関数内の9行目の
console.log(wow)
コマンドは
wow
ではなく
zoom
を表示します。
レキシカルスコープ
JavaScriptインタープリターの機能を調べ、変数と関数を上げるトピックに触れたので、スコープの話に移ります。 レキシカルスコープから始めましょう。 これは、コンパイル時に形成されるスコープであると言えます。 つまり、 このスコープの境界に関する決定はコンパイル時に行われます 。 この記事の目的上、
eval
またはコマンドを使用するコードで発生するこの規則の例外は無視
with
ます。 いずれにしても、これらのコマンドは使用しないでください。
インタープリターの2番目のパスは、変数と関数への値の割り当てが実行されるパスです。 上記のコード例では、このパス中に
bar()
が12行目で呼び出されます。
インタープリターは、この呼び出しを行う前に
bar
宣言を見つける必要があります。これを行うには、現在のスコープで検索を実行します。 その時点で、グローバルスコープは最新です。 最初のパス、つまりコンパイルのおかげで、
bar
宣言がコードの先頭にあることがわかっているため、インタープリターはそれを見つけて関数を実行できます。
8行目を見ると、
console.log(foo);
コマンドがあり
console.log(foo);
、インタープリターはこのコマンドを実行する前に宣言
foo
を見つける必要があります。 彼が最初に行うことは、現在のスコープを調べることです。現在のスコープは、現時点ではグローバルスコープではなく、関数
bar
スコープです。
foo
は関数スコープで宣言されていますか? いいえ、そうではありません。 次に、親スコープまで1レベル上に移動し、そこで変数宣言を探します。 関数が宣言されるスコープは、グローバルスコープです。 fooはグローバルスコープで宣言されていますか? はい、そうです。 したがって、インタープリターは変数の値を取得してコマンドを実行できます。
一般に、レキシカルスコープの意味は、コンパイル後にスコープが決定されることであり、インタープリターが変数または関数宣言を見つける必要がある場合、最初に現在のスコープを調べますが、必要なものを見つけた場合、失敗すると、親のスコープに移動し、同じ原理で検索を続けます。 移動できる最高レベルは、グローバルスコープと呼ばれます。
インタープリターが探しているものがグローバルスコープにない場合、
ReferenceError
エラーがスローされます。
さらに、インタープリターはまず現在のスコープで必要なものを検索し、次に親でのみ検索するため、字句スコープはJavaScriptで変数シェーディングの概念を導入します。 これは、関数の現在のスコープで宣言された変数
foo
が、親スコープで宣言された同じ名前の変数を覆い隠すか隠すことを意味します。 この考えをよりよく理解するには、次の例をご覧ください。
'use strict' var foo = 'foo'; function bar () { var foo = 'bar'; console.log(foo); } bar();
6行目で変数
foo
を宣言すると、3行目で同じ名前の変数の宣言がオーバーライドされるため、このコードはコンソールに
foo
ではなく文字列
bar
を出力します。
変数のシェーディングは、いくつかの変数をマスクし、特定のスコープからの変数へのアクセスを防ぐ必要がある場合に役立つ設計パターンです。 私は通常、この手法の使用を避け、それなしで実行することが絶対に不可能な場合にのみ適用することを避けなければなりません。同じ変数名を使用するとチーム開発で混乱が生じると確信しているからです。 シェーディングを使用すると、開発者が変数が実際に保存されているものではなく保存されていると判断できるという事実につながる可能性があります。
機能範囲
前述のように、字句スコープを考慮して、インタープリターは現在のスコープで変数を宣言します。つまり、関数で宣言された変数は関数スコープで宣言されます。 このスコープは、関数自体とその子孫(この関数内で宣言された他の関数)に制限されます。
関数スコープで宣言された変数には、外部からアクセスできません。 これは非常に強力なデザインパターンであり、プライベートプロパティを作成し、機能スコープ内でのみアクセスできるようにする場合に使用できます。 これは次のようなものです。
'use strict' function convert (amount) { var _conversionRate = 2; // return amount * _conversionRate; } console.log(convert(5)); console.log(_conversionRate); // ReferenceError: _conversionRate is not defined
ブロックスコープ
ブロックスコープは機能に似ていますが、機能に限定されるものではなく、コードのブロックに限定されます。
ES3では、
try / catch
コンストラクトの
catch
式にブロックスコープがあります。つまり、この式には独自のスコープがあります。
try
式にはブロックスコープはなく、
catch
式にのみブロックスコープがあることに注意して
try
。 例を考えてみましょう:
'use strict' try { var foo = 'foo'; console.log(bar); } catch (err) { console.log('In catch block'); console.log(err); } console.log(foo); console.log(err);
このコードは、
bar
にアクセスしようとすると5行目にエラーをスローします。これにより、インタープリターが
catch
式に移動します。
err
変数は式のスコープ内で宣言され、外部からはアクセスできません。 実際、
console.log(err)
行に
err
変数の値を記録しようとすると、エラーが生成されます。 このコードの出力は次のとおりです。
In catch block ReferenceError: bar is not defined (...Error stack here...) foo ReferenceError: err is not defined (...Error stack here...)
変数
foo
は
try / catch
コンストラクトの外部からアクセスできますが、
err
はアクセスできないことに注意してください。
ES6について説明すると、
let
および
const
キーワードを使用する場合、変数と定数は、機能スコープではなく、現在のブロックスコープに暗黙的に付加されます。 これは、
if
ブロック、
for
ブロック、または関数のいずれであっても、これらの構造が使用されるブロックに限定されることを意味します。 これを理解するのに役立つ例を次に示します。
'use strict' let condition = true; function bar () { if (condition) { var firstName = 'John'; // let lastName = 'Doe'; // if const fullName = firstName + ' ' + lastName; // if } console.log(firstName); // John console.log(lastName); // ReferenceError console.log(fullName); // ReferenceError } bar();
let
および
const
キーワードを使用すると、最小開示の原則を使用できます。 この原則に従うことは、変数が可能な限り最小のスコープで利用可能であることを意味します。 ES6より前の開発者は、即時呼び出し関数式(IIFE)で
var
キーワードを使用して変数を宣言するという文体的な方法を使用することにより、ブロックスコープの効果を実現することがよくありましたが、今では
let
と
const
おかげで、機能的なアプローチを取ることができます。 この原則の主な利点のいくつかは、変数への不要なアクセスを回避し、エラーの可能性を減らすことです。 さらに、ガベージコレクターは、ブロックスコープを終了するときに不要な変数からメモリを解放できます。
即座に実行可能な関数式
IIFEは、関数が新しいブロックスコープを作成できるようにする非常に一般的なJavaScriptデザインパターンです。 IIFEは、インタープリターによって処理された直後に実行される通常の関数式です。 IIFEの例を次に示します。
'use strict' var foo = 'foo'; (function bar () { console.log('in function bar'); })() console.log(foo);
このコードは
in function bar
の行を
foo
の出力の前に出力します。これは、
bar
関数がフォーム
bar()
構造を使用して明示的に呼び出す必要なく、すぐに実行されるためです。 これは、次の理由で発生します。
- 関数キーワード(および対応する終了キーワード)の前に開き括弧があります。これにより、この構造が関数宣言から関数式に変わります。
- 関数式はすぐに実行されるため、最後に2つの括弧があります。
すでに見たように、これにより、外部スコープのコードから変数を非表示にしたり、アクセスを制限したり、不要な変数で外部スコープを汚染したりすることがなくなります。
IIFEは、非同期操作を実行していて、変数の状態をIIFEのスコープ内に保持したい場合にも非常に便利です。 この動作の例を次に示します。
'use strict' for (var i = 0; i < 5; i++) { setTimeout(function () { console.log('index: ' + i); }, 1000); }
このコードは0、1、2、3、4を出力することが予想されます。ただし、非同期
setTimeout
操作が呼び出される
for
ループを実行した実際の結果は次のようになります。
index: 5 index: 5 index: 5 index: 5 index: 5
これは、1000ミリ秒が経過するまでにforループが完了し、カウンター
i
が5に等しくなるためです。
コードが期待どおりに機能するように、0から4までの数字のシーケンスを表示するには、IIFEを使用して必要なスコープを保存する必要があります。
'use strict' for (var i = 0; i < 5; i++) { (function logIndex(index) { setTimeout(function () { console.log('index: ' + index); }, 1000); })(i) }
この例では、
i
の値をIIFEに渡します。 関数式には独自のスコープがあり、
for
ループで発生するものは機能しなくなります。 このコードの出力は次のとおりです。
index: 0 index: 1 index: 2 index: 3 index: 4
まとめ
JavaScriptのさまざまな可視性領域を調べ、その機能について話し、いくつかの単純な設計パターンを説明しました。 実際、JavaScriptでスコープについて話すこともできますが、この資料はあなた自身で知識を深め、広げることができる良い基礎を提供すると信じています。
このストーリーがJavaScriptの範囲をよりよく理解し、それによってプログラムの品質を向上させることを願っています。 また、Habréでこの出版物を読むことをお勧めします。
親愛なるJS開発者! JavaScriptでスコープを操作するための興味深いトリックを共有してください。