この章では、まず変数の使用方法を説明し、次に変数の仕組み(環境、クロージャなど)について詳しく説明します。
JavaScriptでは、変数を使用する前にvarステートメントを使用して宣言します:
varfoo;foo=3;// OK, has been declaredbar=5;// not OK, an undeclared variable
宣言と代入を組み合わせて、変数をすぐに初期化することもできます
varfoo=3;
初期化されていない変数の値はundefinedです:
> var x; > x undefined
プログラムの動作を調べるには、2つの角度があります:
プログラムを実行せずに、ソースコードに存在するままの状態で調べます。次のコードが与えられた場合、関数gが関数fの中にネストされているという静的な主張をすることができます
functionf(){functiong(){}}
形容詞レキシカルは静的と同義語として使用されます。なぜなら、どちらもプログラムのレキシコン(単語、ソース)に関係するからです。
プログラムの実行中(「実行時」)に何が起こるかを調べます。次のコードが与えられた場合
functiong(){}functionf(){g();}
f()を呼び出すと、g()が呼び出されます。実行時に、fによってgが呼び出されることは、動的な関係を表します。
変数のスコープとは、アクセス可能な場所のことです。例えば
functionfoo(){varx;}
ここで、xの直接スコープは関数foo()です。
スコープが変数の直接スコープ内にネストされている場合、変数はそれらのすべてのスコープでアクセス可能です
functionfoo(arg){functionbar(){console.log('arg: '+arg);}bar();}console.log(foo('hello'));// arg: hello
argの直接スコープはfoo()ですが、ネストされたスコープbar()でもアクセスできます。ネストに関して、foo()は外側のスコープ、bar()は内側のスコープです。
スコープが、周囲のスコープにある変数と同じ名前の変数を宣言する場合、外側の変数へのアクセスは内側のスコープとその中にネストされたすべてのスコープでブロックされます。内側の変数の変更は外側の変数には影響しません。外側の変数は、内側のスコープが終了した後、再びアクセス可能になります:
varx="global";functionf(){varx="local";console.log(x);// local}f();console.log(x);// global
関数f()内では、グローバルなxはローカルなxによってシャドーイングされます。
ほとんどの主流の言語はブロックスコープです。変数は最も内側の周囲のコードブロックの「内部に存在」します。Javaの例を次に示します。
publicstaticvoidmain(String[]args){{// block startsintfoo=4;}// block endsSystem.out.println(foo);// Error: cannot find symbol}
上記のコードでは、変数fooはそれを直接囲むブロック内でのみアクセス可能です。ブロックの終了後にアクセスしようとすると、コンパイルエラーが発生します。
対照的に、JavaScriptの変数は関数スコープです:新しいスコープを導入するのは関数のみです。ブロックはスコープに関しては無視されます。例えば:
functionmain(){{// block startsvarfoo=4;}// block endsconsole.log(foo);// 4}
言い換えれば、fooはブロック内だけでなく、main()全体でアクセス可能です。
JavaScriptはすべての変数宣言をホイストします。つまり、それらを直接スコープの先頭に移動します。これにより、変数が宣言される前にアクセスされた場合に何が起こるかが明確になります:
functionf(){console.log(bar);// undefinedvarbar='abc';console.log(bar);// abc}
変数barはf()の最初の行に既に存在しますが、まだ値がありません。つまり、宣言はホイストされていますが、代入はホイストされていません。 JavaScriptは、コードが次のようであるかのようにf()を実行します
functionf(){varbar;console.log(bar);// undefinedbar='abc';console.log(bar);// abc}
既に宣言されている変数を宣言しても、何も起こりません(変数の値は変更されません)
> var x = 123; > var x; > x 123
各関数宣言もホイストされますが、方法は少し異なります。変数の作成だけでなく、完全な関数がホイストされます(ホイストを参照)。
JavaScriptのスタイルガイドの中には、ホイストによって騙されないように、変数宣言を関数の先頭にのみ配置することを推奨するものがあります。関数が比較的短い場合(そうでなければなりません)、そのルールを少し緩和して、使用される場所の近く(たとえば、forループ内)で変数を宣言することができます。これにより、コードの一部がより適切にカプセル化されます。もちろん、関数全体のホイストは依然として発生するため、そのカプセル化は概念的なものにすぎないことを認識しておく必要があります。
通常、変数の寿命を制限するために新しいスコープを導入します。 そうしたくなる例としては、ifステートメントの「then」部分が挙げられます。条件が満たされた場合にのみ実行されます。そして、ヘルパー変数を排他的に使用する場合、それらが周囲のスコープに「リーク」することを避けたいのです。
functionf(){if(condition){vartmp=...;...}// tmp still exists here// => not what we want}
thenブロックに新しいスコープを導入する場合、関数を定義してすぐに呼び出すことができます。これはブロックスコープの回避策、シミュレーションです。
functionf(){if(condition){(function(){// open blockvartmp=...;...}());// close block}}
これはJavaScriptでは一般的なパターンです。 Ben Almanは、これを即時実行関数式(IIFE、「イフィー」と発音)と呼ぶことを提案しました。一般に、IIFEは次のようになります
(function(){// open IIFE// inside IIFE}());// close IIFE
IIFEについて注意すべき点がいくつかあります
functionで始まる場合、パーサーはそれが関数宣言であると想定します(式とステートメントを参照)。しかし、関数宣言をすぐに呼び出すことはできません。したがって、ステートメントを開きかっこで始めることで、キーワードfunctionが関数式の始まりであることをパーサーに伝えます。かっこ内には、式のみを含めることができます。 2つのIIFEの間にセミコロンを忘れると、コードは機能しなくなります。
(function(){...}())// no semicolon(function(){...}());
上記のコードは関数呼び出しとして解釈されます。最初のIIFE(かっこを含む)が呼び出される関数であり、2番目のIIFEがパラメーターです。
IIFEはコスト(認知的にもパフォーマンス的にも)がかかるため、ifステートメント内で使用することはほとんど意味がありません。上記の例は、教育的な理由で選択されました。
接頭辞演算子を使用して式コンテキストを強制することもできます。たとえば、論理NOT演算子を使用してこれを行うことができます。
!function(){// open IIFE// inside IIFE}();// close IIFE
または、void演算子を使用して(void演算子を参照)
voidfunction(){// open IIFE// inside IIFE}();// close IIFE
接頭辞演算子を使用する利点は、末尾のセミコロンを忘れても問題が発生しないことです。
既に式コンテキスト内にいる場合、IIFEの式コンテキストを強制する必要はありません。その場合、かっこや接頭辞演算子は必要ありません。例えば:
varFile=function(){// open IIFEvarUNTITLED='Untitled';functionFile(name){this.name=name||UNTITLED;}returnFile;}();// close IIFE
上記の例では、Fileという名前の変数が2つあります。一方では、IIFE内でのみ直接アクセスできる関数があります。他方では、最初の行で宣言されている変数があります。IIFEで返される値が代入されます。
パラメーターを使用して、IIFE内部の変数を定義できます。
varx=23;(function(twice){console.log(twice);}(x*2));
これは次のようになります
varx=23;(function(){vartwice=x*2;console.log(twice);}());
IIFEを使用すると、プライベートデータを関数に添付できます。そうすれば、グローバル変数を宣言する必要がなく、関数とその状態を緊密にパッケージ化できます。グローバル名前空間の汚染を回避できます。
varsetValue=function(){varprevValue;returnfunction(value){// define setValueif(value!==prevValue){console.log('Changed: '+value);prevValue=value;}};}();
IIFEの他のアプリケーションについては、本書の他の場所で言及されています
プログラム全体を含むスコープは、グローバルスコープまたはプログラムスコープと呼ばれます。 これは、スクリプト(Webページの<script>タグであっても、.jsファイルであっても)に入るときのスコープです。グローバルスコープ内では、関数を定義することでネストされたスコープを作成できます。そのような関数内では、再びスコープをネストできます。各スコープは、独自の変数と、それを囲むスコープ内の変数にアクセスできます。グローバルスコープは他のすべてのスコープを囲んでいるため、その変数にはどこからでもアクセスできます。
// here we are in global scopevarglobalVariable='xyz';functionf(){varlocalVariable=true;functiong(){varanotherLocalVariable=123;// All variables of surround scopes are accessiblelocalVariable=false;globalVariable='abc';}}// here we are again in global scope
グローバル変数には2つの欠点があります。第1に、グローバル変数に依存するソフトウェアは副作用の影響を受けやすいです。堅牢性が低く、予測可能性が低く、再利用性が低くなります。
第2に、Webページ上のすべてのJavaScriptは同じグローバル変数を共有します。コード、組み込み関数、分析コード、ソーシャルメディアボタンなどです。つまり、名前の衝突が問題になる可能性があります。そのため、できるだけ多くの変数をグローバルスコープから隠すことが最善です。たとえば、次のようなことはしないでください
<!-- Don’t do this --><script>// Global scopevartmp=generateData();processData(tmp);persistData(tmp);</script>
変数 tmp は、その宣言がグローバルスコープで実行されるため、グローバルになります。しかし、それはローカルでのみ使用されます。したがって、IIFE(IIFEによる新しいスコープの導入を参照)を使用して、ネストされたスコープ内に隠すことができます。
<script>(function(){// open IIFE// Local scopevartmp=generateData();processData(tmp);persistData(tmp);}());// close IIFE</script>
ありがたいことに、モジュールシステム(モジュールシステムを参照)は、グローバル変数の問題をほぼ解消します。モジュールはグローバルスコープを介してやり取りせず、各モジュールはモジュールグローバル変数用の独自のスコープを持っているためです。
ECMAScript仕様では、内部データ構造である環境を使用して変数を格納します(環境:変数の管理を参照)。この言語には、グローバル変数の環境を、いわゆるグローバルオブジェクトと呼ばれるオブジェクトを介してアクセス可能にするという、やや珍しい機能があります。グローバルオブジェクトを使用して、グローバル変数を作成、読み取り、および変更できます。グローバルスコープでは、thisはそれを指します。
> var foo = 'hello'; > this.foo // read global variable 'hello' > this.bar = 'world'; // create global variable > bar 'world'
グローバルオブジェクトにはプロトタイプがあることに注意してください。すべての(自身と継承された)プロパティを一覧表示する場合は、すべてのプロパティキーのリストのgetAllPropertyNames()などの関数が必要です。
> getAllPropertyNames(window).sort().slice(0, 5) [ 'AnalyserNode', 'Array', 'ArrayBuffer', 'Attr', 'Audio' ]
JavaScriptの作成者であるBrendan Eichは、グローバルオブジェクトを自身の「最大の反省点」の1つと考えています。パフォーマンスに悪影響を及ぼし、変数スコープの実装をより複雑にし、コードのモジュール性を低下させます。
ブラウザとNode.jsには、グローバルオブジェクトを参照するためのグローバル変数があります。残念ながら、それらは異なります。
windowが含まれています。これは、ECMAScript 5の一部ではなく、ドキュメントオブジェクトモデル(DOM)の一部として標準化されています。フレームまたはウィンドウごとに1つのグローバルオブジェクトがあります。globalが含まれています。各モジュールには、thisがそのスコープの変数を持つオブジェクトを指す独自のスコープがあります。したがって、モジュール内ではthisとglobalは異なります。どちらのプラットフォームでも、thisはグローバルオブジェクトを参照しますが、グローバルスコープ内にある場合のみです。これはNode.jsではほとんどありません。クロスプラットフォームの方法でグローバルオブジェクトにアクセスする場合は、次のようなパターンを使用できます。
(function(glob){// glob points to global object}(typeofwindow!=='undefined'?window:global));
今後、グローバルオブジェクトを参照するためにwindowを使用しますが、クロスプラットフォームコードでは、前述のパターンとglobを使用する必要があります。
このセクションでは、windowを介してグローバル変数にアクセスするためのユースケースについて説明します。ただし、一般的なルールは、できるだけそれを避けることです。
プレフィックスwindowは、コードがローカル変数ではなくグローバル変数を参照していることを示す視覚的な手がかりです。
varfoo=123;(function(){console.log(window.foo);// 123}());
ただし、これはコードを壊れやすくします。fooをグローバルスコープから別の周囲のスコープに移動するとすぐに、動作しなくなります。
(function(){varfoo=123;console.log(window.foo);// undefined}());
したがって、fooをwindowのプロパティとしてではなく、変数として参照することをお勧めします。fooがグローバル変数またはグローバルのような変数であることを明確にする場合は、g_などの名前プレフィックスを追加できます。
varg_foo=123;(function(){console.log(g_foo);}());
windowを介して組み込みグローバル変数を参照することは好みません。それらはよく知られた名前なので、グローバルであることを示すインジケーターから得られるものはほとんどありません。また、プレフィックス付きのwindowが煩雑になります。
window.isNaN(...)// noisNaN(...)// yes
JSLintやJSHintなどのスタイルチェックツールを使用する場合、windowを使用すると、現在のファイルで宣言されていないグローバル変数を参照したときにエラーが発生しません。ただし、どちらのツールも、そのような変数について通知し、そのようなエラーを防ぐ方法を提供しています(ドキュメントで「グローバル変数」を検索してください)。
これはよくあるユースケースではありませんが、特にシムとポリフィル(シムとポリフィルの比較を参照)は、グローバル変数someVariableが存在するかどうかを確認する必要があります。その場合、windowが役立ちます。
if(window.someVariable){...}
これは、このチェックを実行するための安全な方法です。次のステートメントは、someVariableが宣言されていない場合に例外をスローします。
// Don’t do thisif(someVariable){...}
windowを介してチェックできる2つの追加の方法があります。それらはほぼ同等ですが、もう少し明示的です。
if(window.someVariable!==undefined){...}if('someVariable'inwindow){...}
変数が存在する(そして値を持っている)かどうかを確認する一般的な方法は、typeofを使用することです(typeof:プリミティブの分類を参照)。
if(typeofsomeVariable!=='undefined'){...}
windowを使用すると、グローバルスコープに要素を追加できます(ネストされたスコープにいる場合でも)。また、条件付きで追加することもできます。
if(!window.someApiFunction){window.someApiFunction=...;}
通常、グローバルスコープにいる間に、varを介してグローバルスコープに要素を追加するのが最善です。ただし、windowは、条件付きで追加を行うためのクリーンな方法を提供します。
環境は高度なトピックです。それらはJavaScriptの内部の詳細です。変数の仕組みをより深く理解したい場合は、このセクションを読んでください。
変数は、プログラムの実行がそのスコープに入るときに存在するようになります。その後、ストレージスペースが必要になります。そのストレージスペースを提供するデータ構造は、JavaScriptでは環境と呼ばれます。変数名を値にマッピングします。その構造は、JavaScriptオブジェクトの構造と非常によく似ています。環境は、スコープを離れた後も存続することがあります。したがって、スタックではなくヒープに格納されます。
変数は2つの方法で渡されます。いわば、2つの次元があります。
functionfac(n){if(n<=1){return1;}returnn*fac(n-1);}
関数が何回呼び出されても、常に独自の(新しい)ローカル変数と周囲のスコープの変数の両方にアクセスする必要があります。たとえば、次の関数doNTimesには、ヘルパー関数doNTimesRecが内部にあります。doNTimesRecが自身を複数回呼び出すと、そのたびに新しい環境が作成されます。ただし、doNTimesRecは、これらの呼び出し中もdoNTimesの単一環境に接続されたままです(すべての関数が単一のグローバル環境を共有するのと同じです)。doNTimesRecは、行(1)でactionにアクセスするためにその接続が必要です。
functiondoNTimes(n,action){functiondoNTimesRec(x){if(x>=1){action();// (1)doNTimesRec(x-1);}}doNTimesRec(n);}
これらの2つの次元は次のように処理されます。
識別子を解決するために、アクティブな環境から始まる完全な環境チェーンがトラバースされます。
例を見てみましょう。
functionmyFunction(myParam){varmyVar=123;returnmyFloat;}varmyFloat=1.3;// Step 1myFunction('abc');// Step 2
図16-1は、上記のコードが実行されたときに何が起こるかを示しています。
myFunctionとmyFloatはグローバル環境(#0)に格納されています。myFunctionによって参照されるfunctionオブジェクトは、内部プロパティ[[Scope]]を介してそのスコープ(グローバルスコープ)を指していることに注意してください。myFunction('abc')の実行のために、パラメータとローカル変数を保持する新しい環境(#1)が作成されます。outer(myFunction.[[Scope]]から初期化されます)を介して外部環境を参照します。外部環境のおかげで、myFunctionはmyFloatにアクセスできます。関数が作成されたスコープを離れると、そのスコープ(および周囲のスコープ)の変数に接続されたままになります。例えば:
functioncreateInc(startValue){returnfunction(step){startValue+=step;returnstartValue;};}
createInc()によって返される関数は、startValueへの接続を失いません。この変数は、関数呼び出し間で永続化する状態を関数に提供します。
> var inc = createInc(5); > inc(1) 6 > inc(2) 8
クロージャとは、関数とその関数が作成されたスコープへの接続のことです。この名前は、クロージャが関数の自由変数を「閉じ込める」という事実から来ています。変数は、関数内で宣言されていない場合、つまり「外部から」来た場合、自由です。
これは、クロージャの仕組みをより深く掘り下げた高度なセクションです。環境に精通している必要があります(環境:変数の管理を確認してください)。
クロージャとは、実行がスコープを離れた後も環境が存続する例です。クロージャの仕組みを説明するために、前の`createInc()`とのインタラクションを4つのステップに分割して見てみましょう(各ステップでは、アクティブな実行コンテキストとその環境が強調表示されます。関数がアクティブな場合は、それも強調表示されます)。
このステップは、インタラクションの前、`createInc`の関数宣言の評価後に行われます。`createInc`のエントリがグローバル環境(#0)に追加され、関数オブジェクトを指します。
このステップは、関数呼び出し`createInc(5)`の実行中に行われます。`createInc`のための新しい環境(#1)が作成され、スタックにプッシュされます。その外部環境はグローバル環境(`createInc.[[Scope]]`と同じ)です。環境はパラメータ`startValue`を保持します。
このステップは、`inc`への代入後に行われます。`createInc`から戻った後、その環境を指す実行コンテキストはスタックから削除されましたが、`inc.[[Scope]]`がそれを参照しているため、環境はヒープ上にまだ存在します。`inc`はクロージャ(関数と生成環境)です。
このステップは、`inc(1)`の実行中に行われます。新しい環境(#1)が作成され、それを指す実行コンテキストがスタックにプッシュされました。その外部環境は`inc`の`[[Scope]]`です。外部環境は`inc`に`startValue`へのアクセスを提供します。
このステップは、`inc(1)`の実行後に行われます。`inc`の環境を指す参照(実行コンテキスト、`outer`フィールド、または`[[Scope]]`)はもうありません。したがって、それは必要なく、ヒープから削除できます。
作成した関数の動作は、現在のスコープ内の変数の影響を受けることがあります。JavaScriptでは、各関数は、関数が作成された時点での変数の値で動作するべきであるため、問題となる可能性があります。しかし、関数はクロージャであるため、関数は常に変数の*現在の*値で動作します。`for`ループでは、それが原因で正常に動作しない場合があります。例を挙げると分かりやすくなります。
functionf(){varresult=[];for(vari=0;i<3;i++){varfunc=function(){returni;};result.push(func);}returnresult;}console.log(f()[1]());// 3
`f`は3つの関数を持つ配列を返します。これらの関数はすべて、`f`の環境、つまり`i`にアクセスできます。実際、それらは同じ環境を共有しています。しかし、ループが終了した後、その環境では`i`の値は3になります。したがって、すべての関数は`3`を返します。
これは私たちが望むものではありません。これを修正するには、それを使用する関数を作成する前に、インデックス`i`のスナップショットを作成する必要があります。言い換えれば、各関数を、関数の作成時点での`i`の値とパッケージ化したいのです。そのため、次の手順を実行します。
環境を作成するのは関数だけなので、ステップ1を実行するためにIIFE(IIFEによる新しいスコープの導入を参照)を使用します。
functionf(){varresult=[];for(vari=0;i<3;i++){(function(){// step 1: IIFEvarpos=i;// step 2: copyvarfunc=function(){returnpos;};result.push(func);}());}returnresult;}console.log(f()[1]());// 1
ループを介してDOM要素にイベントハンドラを追加する場合に同様のシナリオが発生するため、この例は現実世界との関連性があることに注意してください。