JavaScriptはブロックに2つのペアの中括弧を使うのが素晴しいですが、これはブロックスコープをサポートしていません 。その為、この言語に残されているのは関数スコープ だけです。
function test() { // スコープ
for(var i = 0; i < 10; i++) { // スコープではない
// 数える
}
console.log(i); // 10
}
注意: 代入が使用されてない時、return文や関数の引数、{...}
表記はブロック文として
解釈されて、オブジェクトリテラルとはなりません 。これはセミコロン自動挿入
と連動して奇妙なエラーを引き起こすことになります。
JavaScriptはまた明確な名前空間を持ちません。この事は全て一つのグローバルで共有された 名前空間で定義されるという事です。
変数が参照されるまでの間、JavaScriptはスコープ全てを遡って参照を探索します。グローバルスコープまで遡っても要求した名前が無いとReferenceError
が発生します。
グローバル変数の致命傷
// スクリプト A
foo = '42';
// スクリプト B
var foo = '42'
上記の2つのスクリプトは同じ効果を持っていません 。スクリプト Aはfoo
と呼ばれる変数を、グローバル スコープに定義しており、スクリプト Bはfoo
を現在 のスコープで定義ています。
繰り返しますが、この2つのスクリプトは同じ影響 を全く持っていない スクリプトになります。var
を使用しない事は重大な意味を持ちます。
// グローバルスコープ
var foo = 42;
function test() {
// ローカルスコープ
foo = 21;
}
test();
foo; // 21
test
関数の中のvar
ステートメントを省略するとfoo
の値をオーバーライドします。最初の内は大した事ではないように思いますが、JavaScriptが何千行規模になると、var
を使っていない事でバグの追跡が酷く困難になります。
// グローバルスコープ
var items = [/* 何かのリスト */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// サブループのスコープ
for(i = 0; i < 10; i++) { // varステートメントが無くなった
// 素敵な実装を!
}
}
外側のループはsubloop
が最初に呼ばれた後に終了します。なぜなら、subloop
がグローバル変数i
の値で上書きされているからです。2番目のfor
ループにvar
を使用する事によって簡単にこのエラーを回避する事ができます。目的とする効果 を外側のスコープに与えようとしない限り、絶対 にvar
ステートメントは省略してはいけません。
ローカル変数
JavaScriptのローカル変数の為の唯一の作成方法はfunction パラメーターとvar
ステートメントによって宣言された変数になります。
// グローバルスコープ
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// 関数testのローカル変数
i = 5;
var foo = 3;
bar = 4;
}
test(10);
foo
とi
は、関数test
のスコープ内のローカル変数ですが、bar
の代入は同じ名前でグローバル変数で上書きしてしまいます。
巻き上げ
JavaScriptは宣言を巻き上げ ます。これはvar
ステートメントとfunction
宣言が、それらを含むスコープの一番先頭に移動するという事を意味します。
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
上記のコードは、実行を開始する前に変換されてしまいます。JavaScriptはvar
ステートメントと同じように、直近で囲んでいるfunction
宣言を先頭に移動させます。
// varステートメントはここに移動する
var bar, someValue; // 'undefined'がデフォルト
// function宣言もここに移動する
function test(data) {
var goo, i, e; // 無くなったブロックスコープはこちらに移動する
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // barが'undefined'のままなので、Typeerrorで呼び出し失敗
someValue = 42; // 割り当てすると巻き上げの影響を受けない
bar = function() {};
test();
ブロックスコープの欠落はvar
ステートメントをループやボディの外に移動するだけでなく、if
の構成を直感的ではないものにしてしまいます。
元のコードの中のif
ステートメントはグローバル変数 であるgoo
も変更しているように見えますが、実際には -巻き上げが適用された後に- ローカル変数 を変更しています。
巻き上げ についての知識がないと、下に挙げたコードはReferenceError
になるように見えます。
// SomeImportantThingが初期化されているかチェックする
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
しかし、勿論上記の動きはvar
ステートメントがグローバルスコープ の上に移動しているという事実に基づいています。
var SomeImportantThing;
// 他のコードがSomeImportantThingをここで初期化するかもしれないし、しないかもしれない
// SomeImportantThingがある事を確認してください
if (!SomeImportantThing) {
SomeImportantThing = {};
}
名前解決の順序
JavaScriptのグローバルスコープ を含む、全てのスコープは、現在のオブジェクト を参照している特殊な名前this
を持っています。
関数スコープはまた、arguments
という名前も持っています。それは関数スコープの中で定義され、関数に渡された引数を含んでいます。
例として、関数の中でfoo
と命名された変数にアクセスしようとする場合を考えましょう。JavaScriptは以下の順番で、その名前を探索しようとします。
var foo
ステートメントが現在のスコープで使われている場合
foo
という名前の関数パラメーターが存在するかどうか
関数それ自体がfoo
として呼ばれているかどうか
一つ外のスコープに行き、再度#1 から始める
名前空間
一つしかグローバルの名前空間を持たない事による良くある問題は変数名の衝突による問題の起きる可能性です。JavaScriptでは、この問題を匿名関数ラッパー の助けで簡単に回避できます。
(function() {
// "名前空間"に自分を含む
window.foo = function() {
// 露出したクロージャ
};
})(); // 即座に関数を実行する
無名関数はexpressions とみなされ、呼び出し可能になり最初に評価されます。
( // カッコ内の関数が評価される
function() {}
) // 関数オブジェクトが返される
() // 評価の結果が呼び出される
関数式を評価し、呼び出す別の方法として構文は違いますが、同様の動作をするのが下記です。
// 2つの別の方法
+function(){}();
(function(){}());
終わりに
自身の名前空間にカプセル化する為に常に匿名関数ラッパー を使用する事を推奨します。これは、コードを名前衝突から守る為だけでなく、プログラムのより良いモジュール化の為でもあります。
さらに、グローバル変数の使用は悪い習慣 と考えられています。一回 でもグローバル変数を使用するとエラーが発生しやすく、メンテナンスがしにくいコードになってしまいます。