JavaScriptスコープ

JavaScriptでは、スコープは重要ですが、物議を醸す概念です。 スコープは、適切なアプローチで使用されるため、信頼性の高いデザインパターンを使用して、プログラムでの望ましくない副作用を回避できます。 この記事では、JavaScriptでさまざまなタイプのスコープを分析し、それらがどのように機能するかについて説明します。 このメカニズムをよく理解すれば、コードの品質を改善できます。





クエリ「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()



構造を使用して明示的に呼び出す必要なく、すぐに実行されるためです。 これは、次の理由で発生します。





すでに見たように、これにより、外部スコープのコードから変数を非表示にしたり、アクセスを制限したり、不要な変数で外部スコープを汚染したりすることがなくなります。



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でスコープを操作するための興味深いトリックを共有してください。



All Articles