公開日:2008.02.04
変数のスコープ
JavaScriptにはブロックスコープがない
まずは以下のコードを見てください。
(function() {
for (var i = 0; i <= 10; i++) {
var j = i;
}
alert(i + j);
})();
このコードを実行すると何と表示されるでしょう?
CやJavaの感覚で答えるとすれば『文法エラー』です。
しかし大方の予想を裏切り、答えは19が表示されます。
var を付けて宣言したローカル変数は関数内のどこで宣言しようと、 その関数全体で参照できてしまいます。 それは例えfor文内であっても同じです。 同様にwhile文でもswitch文でも無名のブロック内でもです。 しかし関数内ではその限りではありません。
(function() {
var f2 = function() {
var i = 100;
};
alert(i);
})();
これは普通にエラーとなります。 同様に一番外側のグローバル領域にも同じ事が言えます。
function f() {
var j = 5;
}
alert(j); //-> エラー
一番外側を一つの大きな関数と考えると分かり易いかもしれません。 このことからJavaScriptでは関数毎に変数のスコープを持っているといえます。
また先の例で関数内のどこで変数を宣言しても参照できると言いましたが、 その変数に値を入れる事なく参照をするとどうなるでしょうか?
(function() {
alert(k);
var k = 100;
})();
実はこれはエラーにはなりません。
関数内にvar kという文がある時点でその関数内において
kという変数は宣言されいます。
しかし、その値はまだ代入されていません。
よってこのコードの実行結果は「undefined」と表示されます。
ここで一つ注意が必要なのは関数内スコープになるのは、 varで宣言した変数のみです。 ではvarを付けずに変数に値を代入した場合はどうなるのでしょう? 知っての通りwindowのプロパティとなります。 なのでこんなコードも成り立ちます。
(function() {
tmp = 100; // var を付けずにtmpに100を代入
})();
(function() {
alert(tmp); // 100と表示
})();
これは一つ目の関数の実行時にvarを付けずにtmp変数に値を代入したので、 window.tmpに100を代入した事になります。 次の関数内でのtmpの参照はwindow.tmpの参照となるので100が表示されます。
また一番外側での変数はグローバル変数となるので上記のコードは、 以下のように書くのと同じです。
var tmp = 100;
(function() {
alert(tmp); // 100と表示。
})();
こちらの場合は直観的でしょうか。 ただこのtmpはwindow.tmpだという事を理解しておいてください。 ですので一番外側ではvar を付ける必要がありません。
レキシカルスコープとスコープチェイン
以前一度触れましたがレキシカルスコープについて説明します。 関数内での変数の参照はソースコード上に宣言された変数を 静的に参照します。ピンとこないかもしれませんね。 ではコードで示します。
var num = 100;
function makefunc() {
return function() {
alert(num);
}
}
function callfunc() {
var num = 50;
var func = makefunc();
func();
};
callfunc();
このコードの実行結果は何と表示されるでしょう。 50だと思った方は間違い。答えは100です。 関数内からの参照はあくまでもソースコード上に 記述された一番近い変数を参照します。 このスコープの事をレキシカルスコープといいます。
では次に関数のネストをもっと深くしてみましょう。
var num = 100;
function makefunc_out() {
var num = 50;
var makefunc_in = function() {
var num = 20;
return function() {
alert(num);
}
}
return makefunc_in();
}
function callfunc() {
var func = makefunc();
func();
}
callfunc();
このコードの実行結果は20と表示されます。 makefuncで返された関数内での変数の参照ははソースコード上の最も直近の変数にアクセスします。 この場合は一つ外側にあるnum = 20となります。 var num = 20を宣言してない場合はもう一つ外側のnum = 50を参照します。 当然といえば当然ですよね。 この順に近いスコープから辿っていって最初に見つかった変数を参照する事をスコープチェインといいます。
Javaをご存知の方のために、もう少しわかりやすく例えてみましょう。 Javaではクラスのインスタンス変数にはthisを付けてアクセスします。 またthisを付けずにアクセスする事もできます。 しかしメソッドの引数もしくはローカル変数にインスタンス変数と同じ名前で変数を宣言した場合、 thisを付けずにインスタンス変数を参照しようとするとローカル変数を優先して参照します。 そのイメージと同じです。
関数スコープとレキシカルスコープを合わせる
上で説明した内容を組み合わせて少し不思議なコードをお見せしましょう。
var num = 100;
function func() {
(function() {
alert(num);
}();
};
func();
これは普通に100が表示されます。 では以下のように変更するとどうなるでしょう?
var num = 100;
function func() {
(function() {
alert(num);
}();
var num = 50;
};
func();
答えは「undefined」となります。驚きましたか? 順を追って上の関数を説明すると、
- alert(num)でnumにアクセスする。
- 一つ上のfunc関数のスコープ内でnumを探す。
- 値の代入はされていないが同関数内にある宣言var num = 50;で func関数スコープにはnumが存在することになるのでその値を参照。
- 一番内側の関数が実行された時点ではまだnumに50は代入されていない。
- よってundefined値となる。
という事なんです。
レキシカルスコープはソースコード上の記述順ですが、
関数スコープ毎の直近の変数値を参照します。
そして見つかるまで順に外側のスコープの値を参照します。
今回説明したようにJavaScriptの変数参照メカニズムは少々複雑なところがあります。 ですので十分に気を付ける必要があるでしょう。
まとめ
- ブロックによる変数のスコープは存在しない
- 関数毎に独立したスコープがある
- 参照した変数がその関数内に無い場合、ソースコードの記述上、 外側の関数のスコープを順に参照する
