JavaScript のスコープを理解する
スコープとは、変数の有効範囲のことで、プログラムのどの場所から参照できるかを決める概念です。
スコープの種類
JavaScript のスコープには、グローバル変数とローカル変数の 2 種類あります。
グローバル変数 | ローカル変数 |
---|---|
関数の外(トップレベル)で宣言した変数 | 関数の中で宣言した変数, 関数の仮引数 |
プログラム全体から参照できる | その関数の中でのみ参照できる |
ブロックスコープは存在しない
Java などの言語では、if や for などの {} で囲まれたブロックごとにもブロックスコープがありますが、JavaScript には存在しません。 JavaScript でどうしてもブロックスコープを使いたい場合は、with 命令を使う方法や、無名関数を定義と同時に呼び出すなどの方法で、擬似的にブロックスコープを作ることは可能です。
補足 : let を使うとブロックスコープがつくれると教えてもらったのですが、ちゃんと調べてないので、調べたらここに追記します!
スコープの役割
- 同じ名前の変数が、意図せず競合することを避ける。
スコープが違う場合、同じ名前の変数であっても別物として扱われる。 - ローカル変数の記憶領域は、関数の実行が終わり次第、破棄される。
グローバル変数はプログラムが終了するまで記憶領域を確保するため、関数内でしか必要ない変数までグローバル変数にした場合、無駄にメモリを消費することになる。
宣言時の var の有無
var を省略して変数宣言をした場合、関数内での宣言であっても、その変数はグローバル変数になってしまいます。 混乱を避けるため、変数宣言の var は必ずつけること。
ローカル変数の有効範囲
ローカル変数は「関数全体で有効」なので、同じ関数内であれば、変数宣言より前のコードからアクセスできます。 これをホイスティング(巻き上げ)と呼びます。
var scope = "Global"; function getValue() { console.log(scope); // > undefined var scope = "Local"; return scope; } console.log(getValue()); // > "Local" (ローカル変数を参照) console.log(scope); // > "Global" (グローバル変数を参照)
getValue 関数の console.log(scope);
は、変数宣言 var scope = "Local";
より前にありますが、ホイスティングによってグローバル変数の scope ではなく、ローカル変数の scope を参照しています。
ただし、変数宣言はホイスティングされますが、代入はされないために undefined が返っています。
ホイスティングとは、JavaScript の内部で、上記コードの getValue 関数を、下記のように書き換えているようなイメージです。
function getValue() { var scope; // ホイスティングで関数宣言だけを先頭に移動 console.log(scope); // > undefined scope = "Local"; // 代入は元の位置にそのまま残る return scope; }
Java などのブロックスコープがある言語では、できるだけスコープを小さくするために、必要になった場所で変数宣言をしますが、JavaScript にはブロックスコープがないので、関数内の先頭に変数宣言を持っていった方が、直感的なスコープと実際のスコープが一緒になるので分かりやすいです。
関数の入れ子
関数を入れ子にして定義すると、それぞれの関数ごとに独自のローカルスコープを持ちます。 内側と外側のローカル変数が同じ名前の場合は、内側のローカル変数が優先されます。
仮引数のスコープ
仮引数もローカル変数です。
var n = 0; function incrementValue(n) { n++; return n; } console.log(incrementValue(n)); // > 1 (ローカル変数を参照) console.log(n); // > 0 (グローバル変数を参照)
incrementValue 関数の仮引数は、グローバル変数と同じ名前ですが、incrementValue 関数内のみで有効で、グローバル変数に影響は与えません。 また、incrementValue 関数の外から変数 n を参照した場合は、グローバル変数を参照します。 スコープの外から、スコープの中の変数を参照することはできません。
同じ名前でも別の変数として扱われるというのはつまり、仮引数の名前を変えた下記のコードと同じことです。
var n = 0; function incrementValue(m) { // n を m にするのと同じ意味 m++; return m; } console.log(incrementValue(n)); // > 1 (ローカル変数を参照) console.log(n); // > 0 (グローバル変数を参照)
引数で値を渡すということは、渡した先の関数のローカル変数に、渡した値をコピーしているようなイメージです。 よって、コピー先(ローカル変数)をいくら変更しても、コピー元(グローバル変数)に影響は与えません。
仮引数で参照型を渡したときのスコープ
参照型とは、値そのものではなく、その値があるメモリ上の番地(アドレス)が格納される変数のことです。 関数 function、配列 array などは参照型なので、これらの変数には実際のオブジェクトが格納されているわけではなく、別の場所にあるオブジェクトのアドレス情報が格納されているだけです。
引数で参照型を渡すということは、オブジェクトそのものをコピーしているわけではなく、アドレス情報をコピーしているだけなので、元々のオブジェクトはひとつのままです。
var ary = [1, 2]; function addValue(ary) { ary.push(3); return ary; } console.log(addValue(ary)); // > [1, 2, 3] (ローカル変数を参照) console.log(ary); // > [1, 2, 3] (グローバル変数を参照)
仮引数で渡されたアドレスにあるオブジェクトの値を関数内で変更すると、グローバル変数からも同じオブジェクトを参照するので、どちらも値が変わっています。仮引数はローカル変数ですが、参照渡しの場合はスコープの話からは外れます。
まとめ
- グローバル変数はプログラム全体から参照でき、ローカル変数はその関数の中でのみ参照できる。
- ローカル変数は var の有無でスコープが変わるので、変数宣言の var は必ずつける。
- ホイスティングによる混乱を避けるため、関数内の先頭に変数宣言をまとめて書く。
- 仮引数もローカル変数のスコープだが、参照渡しの場合は関数外から参照するオブジェクトにも影響を与えるので気をつける。
長くなったので、スコープチェーンとクロージャについては分割して書きます。たぶん今日中に。
→ この話の続き : JavaScript のスコープチェーンとクロージャを理解する