変数を利用する場合、そのスコープ(Scope)を意識することは重要だ。スコープとは、プログラム中での変数の有効範囲のこと。JavaScriptのスコープは、大きく「グローバル・スコープ」と「ローカル・スコープ」とに分類できる。
グローバル・スコープとはプログラム中のどこからでも有効な変数の範囲を、ローカル・スコープとはその変数が宣言された関数の中でのみ有効な変数の範囲を表すスコープのことだ。また、グローバル・スコープに属する変数のことを「グローバル変数」、ローカル・スコープに属する変数のことを「ローカル変数」と呼ぶ。
とまあ、この辺までは多くのプログラミング言語で共通の、ごく常識的な知識の範囲なので、読者諸氏にとっても「何をいまさら」といったところかもしれないが、ここでは確認の意味も込めて、具体的なコードでもって動作を確認してみることにしよう。
以下は、関数の内外でそれぞれ同名の変数numを定義した例だ。
var num = 1; [A]
function localFunc() {
var num = 0; [B]
return num;
}
window.alert(localFunc()); // 0 [C]
window.alert(num); // 1 [D]
JavaScriptでは、関数の外で定義された変数はグローバル変数、関数内部で定義された変数はローカル変数と見なされる。つまり、リスト7では[A]で定義された変数numはグローバル変数、[B]で定義された変数numがローカル変数ということになる。同名であっても、スコープの異なる変数は異なるものと見なされる点にあらためて注目してほしい。
果たして、ユーザー定義関数localFuncの呼び出し([C])では、関数内で定義されたローカル変数numの値として「0」が、[D]ではグローバル変数numの値として「1」が、それぞれ出力されることが確認できるはずだ。
以上がグローバル変数とローカル変数の基本的な挙動であるが、いくつか注意すべき点もある。以下では、JavaScriptで変数を扱う場合に知っておきたい――スコープにかかわるポイントをまとめておくことにしよう。
■ローカル変数はvarキーワードで宣言する
例えば、以下のようなコードを見てみよう。リスト8は、先ほどのリスト7においてはvarキーワードで宣言していた変数を、varキーワードを省略して宣言したものだ(太字部分)。
num = 1; [A]
function localFunc(){
num = 0; [B]
return num;
}
window.alert(localFunc()); // 0
window.alert(num); // 0 [C]
前述したように、JavaScriptでは変数宣言におけるvarキーワードは省略可能であるので、このリスト8は構文的には正しいコードである。しかし、実際にコードを実行してみるとどうだろう。[C]の個所で、先ほどと異なる結果が確認できるはずだ。
結論からいってしまうと、JavaScriptではvarキーワードが省略された場合、その変数はグローバル変数と見なされる。
つまり、関数内で宣言された変数num([B])もグローバル変数と見なされ、[A]で定義されたグローバル変数numの値を上書きしてしまうというわけだ。結果、[C]では上書きされたグローバル変数numの値「0」が出力されることになる。
もっとも、関数内部で意図せずにグローバル変数を書き換えてしまうのは(当然)好ましいことではない。関数内では原則として、
すべての変数をvarキーワード付きで定義する
ようにしておきたい。
ちなみに、グローバル変数では、こうしたvarキーワードの有無による挙動の違いは発生しない。しかし、varキーワードを付けておけば、それが変数の宣言であることが視覚的にも確認しやすくなるし、そもそもグローバル変数ではvarキーワードを付けない、ローカル変数ではvarキーワードを付ける、というのもかえって間違いのモトとなるので、基本的には「すべての変数宣言はvarキーワード付きで行う」ことを強くお勧めしたい。
■ローカル変数は関数全体で有効である
先ほど、ローカル変数は「宣言された関数内で有効である」と述べたが、より厳密には「宣言された関数<全体>で有効である」というのが正しい。このやや不可思議な特徴を理解するために、以下のコードをご覧いただきたい。
var num = 1;
function localFunc() {
window.alert(num); // 表示結果は? [A]
var num = 0; [B]
return num;
}
window.alert(localFunc()); // 0
window.alert(num); // 1
このリスト9は、先ほどのリスト7に[A]の部分を追加しただけのものだ。さて、ここでクイズ。[A]の段階で出力される値はいくつになるだろう。
まだローカル変数numの宣言([B])が実行される前なので、グローバル変数numの値である「1」が表示されると考えた方、残念ながらハズレである。
繰り返すが、JavaScriptではローカル変数は「宣言された関数<全体>」で有効となる。つまり、この場合、ユーザー定義関数localFuncの中で定義されたローカル変数numが、関数の先頭から有効であるというわけだ。しかし、[A]の時点ではまだvar命令が実行されていないので、ローカル変数numは初期化されていない。
ということで、先ほどのクイズの答えは「undefined(未定義)」となるわけだ。これはJavaScriptのやや分かりにくい挙動でもあり、時として思わぬ不具合の原因となる可能性もある。これを避けるという意味でも、
ローカル変数は関数の先頭で宣言する
ようにすることが好ましい。
■ブロック・レベルのスコープは存在しない
Visual BasicやC#、C++のような言語とは異なり、JavaScriptではブロック・レベルのスコープが存在しない点にも要注意だ。例えば、以下にC#によるごく基本的なコードを挙げてみよう。
// C#
for (int i = 0; i < 10; i++) {
// ブロック内の処理
}
Response.Write(i); // エラー [A]
forブロック内で定義された変数iはブロック配下でのみ有効であるので(ブロック・レベルのスコープを持つため)、ブロック外(ここでは[A])で変数iを参照しようとすると、コンパイル時にエラーが発生するわけだ。
しかし、JavaScriptで同様のコードを記述するとどうだろう。以下のリスト11は、リスト10のコードをJavaScriptで書き直したものだ。
for (var i = 0; i < 10; i++) {
// ブロック内の処理
}
window.alert(i); // 表示結果は? [A]
Visual BasicやC#、C++など、ブロック・レベルのスコープを持つ言語に慣れた方にとっては、直感的に[A]の部分で実行時エラーが返されると思われるかもしれない。しかし、リスト11の実行結果は「10」となる。繰り返しであるが、JavaScriptではブロック・レベルのスコープが存在しないために、forループを抜けた後も変数iの値はそのまま保持されるのだ。
もっとも、意図せぬ変数の競合を防ぐという意味でも、スコープは一般的にできるだけ必要最小限にとどめるのが好ましい。そのような場合には、JavaScriptでも、前回紹介した匿名関数を利用することで、ブロック・レベルのスコープを疑似的に表現することが可能だ。
(function() {
for (var i =0; i < 10; i++) {
// ブロック内の処理
}
})();
window.alert(i); [A]
太字の部分はやや見慣れない構文であるかもしれないが、要は以下のコードと同意である。
var f = function() {
for (var i = 0; i < 10; i++) {
// ブロック内の処理
}
};
f();
前回述べたように、JavaScriptの関数とはそれ自体がオブジェクトであるので、このように匿名関数自体を式の一部として利用するような書き方もできてしまうというわけだ*1。
*1 ちなみに、リスト12のような記述は開発時に関数単体の動作を確認したいという場合にも有効な機能だ。関数定義全体をカッコでくくり、その末尾に「();」や「(引数);」のように動作に必要な情報を付与すればよいだけなので、よりシンプルに記述できる。
リスト12のように匿名関数の中で定義された変数は、ローカル変数として認識されるので、外部とは隔離することができる。意味のない匿名関数の定義が直感的に分かりにくいという難点こそあるものの、実際にリスト12を実行してみると、確かに[A]の個所で「'i'は宣言されていません」という実行時エラーが発生し、スコープが隔離されていることを確認できる。
[参考]匿名オブジェクトによる疑似ブロック・スコープ
本文では、匿名関数を用いた疑似ブロック・スコープの記法を紹介したが、そのほかにもwithブロックと匿名オブジェクトを利用して、疑似ブロック・スコープを定義することが可能だ。
with ({i:0}) {
for (i = 0; i < 10; i++) {
// ブロック内の処理
}
}
window.alert(i);
匿名オブジェクトについては次回あらためて解説する予定であるので、ここでは「{i:0}」でiという名前のプロパティ(値は0)を持つ匿名オブジェクトを定義しているとだけ理解しておけばよいだろう。
with命令は、ブロック内で共通して利用するオブジェクトを指定するものだ。with命令を利用することで、ブロック内で指定されたオブジェクトのメンバにアクセスする場合、「オブジェクト名.メンバ名」ではなく「メンバ名」で直接記述することが可能になる。つまり、リスト13の例でいうならば、「i = 0」は実際には「obj.i=0」(変数objはオブジェクト変数とする)を表しているわけだ。
リスト12との違いは、「function」が「with」に置き換わっただけではあるが、意味的にはより直感的に理解しやすいコードになったと感じるのではないだろうか。匿名オブジェクトによる疑似構文は、匿名関数による疑似構文に比べると若干速度に劣るというデメリットもあるが、通常のスクリプトであればこちらの匿名オブジェクト構文を、パフォーマンスを強く意識したい局面では匿名関数構文を、というような使い分けをするとよいだろう。
Copyright© Digital Advantage Corp. All Rights Reserved.