このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

プログラマのためのJavaScript (12):不思議な宣言と奇妙なスコープ

ひさびさに「プログラマのためのJavaScript」。あいだは空きましたが、予定どおりスコーピングを話題にします。JavaScriptには“変なところ”がいくつもありますが、そのなかでも、スコーピングはもっとも混乱と弊害をまねくところではないでしょうか。これを読めば、間違うことも悩むこともなくなりますよ。

[追記]僕の誤認と勘違いをコメントでご指摘いただきました。いつも、ありがとうございます。ご指摘を本文に反映しました。変更部分は取消線を使って修正、または「追記」と明示してあります。ただし、ついでに表現を直した部分までは明示してません。[/追記]

今回の内容:

  1. JavaScriptにおける宣言文
  2. undefined値
  3. var文はこのように働く
  4. 驚くべき現象
  5. 疑似ブロックと将来の仕様変更
  6. 今回のまとめ

JavaScriptにおける宣言文

プログラムは文(複合文も含む)の並びです。多くのプログラミング言語では、宣言文と実行文の区別があり、宣言文はコンパイラに情報を与えたり、コンパイル動作を指示したりします。JavaScriptコンパイル・フェーズがある(第5回「コンパイル単位」を参照)ので、宣言文があります。function文とvar文が宣言文だといっていいでしょう。

function文は、関数定義本体をコンパイルしたコード(を持つ関数オブジェクト)を大域オブジェクトのプロパティ(その名前は関数名)としてセットすることをコンパイラに指示します。よって、実行時には、最初から大域関数は使える状態になっています。このことから、function文は、確かに宣言文と呼ぶにふさわしいものです。

では、var文はどうでしょう。変数宣言文と呼ぶにふさわしいでしょうか。次のプログラム(コンパイル単位)を実行してみてください。


if (typeof f != 'undefined') alert("function 'f' is defined.");
if (typeof x != 'undefined') alert("variable 'x' is defined.");
if (typeof y != 'undefined') alert("variable 'y' is defined.");

function f() {}
var x = "hello";
var y;

プログラムの入り口で関数fは既に存在しますが、変数x, yは存在しない(undefinedな)ようですね。となると、var文は実行文なのでしょうか。そうとも言い切れないのです。次のプログラムを、Firefox(または、alertを定義したRhino)で実行してみましょう(IEではダメです[追記]IEでも動くものを節の最後に追加しました[/追記])。


if (this.hasOwnProperty('f')) alert("function 'f' is allocated.");
if (this.hasOwnProperty('x')) alert("variable 'x' is allocated.");
if (this.hasOwnProperty('y')) alert("variable 'y' is allocated.");

function f() {}
var x = "hello";
var y;

hasOwnPropertyメソッドは、オブジェクトのプロパティの存在を確実に判定します(IEにはないけどIEにもありますが、windowオブジェクトに対しては期待通りに動いてくれません)。この結果から、var文もやはり宣言文であることが分かります。コンパイル終了の段階(実行の直前)で、関数も変数も準備ができているのです。これは、関数宣言や変数宣言が、ソースコードどこに書かれていようと関係ありません。次のプログラムも確認してください。


if (this.hasOwnProperty('f')) alert("function 'f' is allocated.");
if (this.hasOwnProperty('x')) alert("variable 'x' is allocated.");
if (this.hasOwnProperty('y')) alert("variable 'y' is allocated.");

function f() {}
if (window.notDefinedOrAlwaysFalse) {
var x = "hello";
} else {
var y;
}

[追記]


if ('f' in this) alert("function 'f' is allocated.");
if ('x' in this) alert("variable 'x' is allocated.");
if ('y' in this) alert("variable 'y' is allocated.");

function f() {}
if (window.notDefinedOrAlwaysFalse) {
var x = "hello";
} else {
var y;
}

[/追記]

●undefined値

話を先に進める前に、undefined値について触れておきます。JavaScriptでは、未定義を意味する“値”が存在します、それがundefined値です。undefined値は0ともnullともNaN(not a number)とも異なります。が、リッパな値なのです。undefined値の型はundefined型であり、undefined型に属する唯一の値がundefined値です。

undefined値を表現するリテラルは仕様で定義されていませんが、(void 0)が使えます。あるいは、初期化されてない変数をundefinedリテラルの代わりにできます。また、キーワードundefinedを使える処理系も多いようです;undefined値を値とする'undefined'という名前の大域プロパティが存在します。


js> var UNDEFINED
js> typeof UNDEFINED
undefined
js> (void 0) === UNDEFINED
true
js>

存在しない変数/プロパティの型をtypeofで調べると(文字列)'undefined'が返ってきます。これは、変数/プロパティが存在しその値が値がundefined値である状況と区別できません。変数/プロパティの存在を確認するには、for文で列挙して;in演算子で調べるか、先に出現したhasOwnPropertyメソッドを使う必要があります(しかし、この2つの方法の結果は必ずしも一致しません、inは__proto__チェーンをたどります)。

typeofに存在しない変数を渡すことはできますが、存在しない変数にアクセスするとエラーになるので、次の2つは等価ではありません。


// someVarが存在しなくても大丈夫
if (typeof(someVar) == 'undefined') alert("someVar is undefined");

// someVarが存在しないとエラー
if (someVar == (void 0)) alert("someVar is undefined");

●var文はこのように働く

var文は、コンパイル時に変数を確保する働きをします。ただし、変数の初期化はしません。いやっ、undefined値で初期化しているともいえますね。

ここで“変数”といっているのは、スコープ(と呼ばれるオブジェクト)のプロパティのことです。JavaScriptのスコープは、原則として大域スコープ(大域オブジェクト)と関数内局所スコープ(呼び出しオブジェクト)しかありません。クロージャイベントハンドラ(ブラウザ固有)の2つは例外ですが、今考える必要はありません。

次の(var文が作為的に多い)プログラムを見てください。


var a = [2, -1, 0, 10, -5, 3];
var result = 0;
for (var i = 0; i < a.length; i++) {
var x = a[i];
if (x > 0) {
var sq = x * x;
result += sq;
}
}
alert(result);

これは、次のプログラムと等価です。変数宣言はコンパイル時に処理されるので、STARTと書いてあるところから実行が開始されると思ってください。


var a;
var result;
var i;
var x;
var sq;

/* === START === */

a = [2, -1, 0, 10, -5, 3];
result = 0;
for (i = 0; i < a.length; i++) {
x = a[i];
if (x > 0) {
sq = x * x;
result += sq;
}
}
alert(result);

●驚くべき現象

今までの説明から、JavaScripにはブロックスコープがないことがわかったでしょう。これは、かなり奇妙な現象を引き起こします。次の例を見てください。


function fun1(arg) {
x = arg;
return x + x;
}

function fun2(arg, flag) {
x = arg;
if (flag) {
var x = arg + arg;
return x;
}
return x;
}

fun1の変数xは関数内で宣言されていません。こんなときxは大域変数を指すのです。つまり、fun1は、大域変数に引数値をセットして、その2倍を戻り値で返します。


js> typeof x
undefined
js> fun1(5)
10
js> typeof x
number
js> x
5
js>

さて、fun2ですが、これは第2引数がtrueならfun1と同じ動作で、そうでないなら第1引数argと変数xと戻り値が同じ値になります -- と、そう思ったでしょ、あなた(あなた=フツウの人)。


js> x = 0
0
js> fun2(5, true)
10
js> x
0
js> fun2(5, false)
5
js> x
0
js>

あれれれ、大域変数xには何の影響も与えません。

そうです。関数定義においても、局所変数は実行前に準備されます。変数宣言(var文)がブロックのなかにあっても、そんなことには関係なくプログラム冒頭に宣言が集約されてしまうのです。ですから、fun2は、次と等価です。


function fun2(arg, flag) {
var x;
x = arg;
if (flag) {
x = arg + arg;
return x;
}
return x;
}

ブロックスコーピングに慣れていると、これは相当にショッキングな事実です。

●疑似ブロックと将来の仕様変更

ブロックスコープがないことはかなりの混乱を招きます。例えば、先に挙げた例(下に再掲)において、変数i, x, sqなどはループの外でも存在し続けます。


for (var i = 0; i < a.length; i++) {
var x = a[i];
if (x > 0) {
var sq = x * x;
result += sq;
}
}

一時的な変数でスコープを汚してしまうことを避けるには、function式を使った疑似ブロックスコープを使えます。


var a = [2, -1, 0, 10, -5, 3];
var result = 0;
(function () {
for (var i = 0; i < a.length; i++) {
var x = a[i];
if (x > 0) {
var sq = x * x;
result += sq;
}
}
})();
alert(result);

こうすると、i, x, sqは関数内局所スコープで定義されるので、外部まで漏れ出ることはありません。

こんなトリックがあるとはいえ、ブロックスコープの欠如が問題を引き起こすのは明らかなので、JavaScript 2.0(ECMAScript 第4版)ではブロックスコープを導入します(せざるを得ない)。また、x = arg;のような代入文で新しい変数がその場で生成される仕様も芳<かんば>しくないので、strictモードでは、"variables must be declared" となるそうです。

●今回のまとめ

  1. JavaScriptのfunction文とvar文は宣言文である。
  2. 関数と変数はコンパイル時に準備され、実行時には最初から存在し、利用可能になっている。
  3. 実行開始時点では、変数の値はundefined値である。
  4. JavaScriptには、大域スコープと関数内局所スコープがある。
  5. var文がプログラムのどこに置かれても記述位置に無関係に、プログラム(トップレベルコードまたは関数定義コード)先頭に変数宣言があるとみなされる。
  6. したがって、JavaScriptブロックスコープは存在しない。
  7. (function () { ... })() とすると、擬似的なブロックスコープを実現できる。

次回は、スコーピングに関連して、関数内関数とクロージャについて述べるつもりです(さて、いつになるかな?)