檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
ところで、アーカイブってけっこう便利ですよ。タクソノミーも作成中。

2005-12-09 (金)

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

| 08:55 | プログラマのための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 () { ... })() とすると、擬似的なブロックスコープを実現できる。

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

山本山本 2005/12/09 20:01 hasOwnPropertyはIEにもあります。
http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthhasownproperty.asp
IEはwindowオブジェクトの扱いが特殊なようです。

nanto_vinanto_vi 2005/12/09 23:12 リテラルではありませんがECMAScript/JavaScriptではグローバルオブジェクトにundefinedプロパティ(値はundefined)が存在します。
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/15-1_Global_Object.html#section-15.1.1.3
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Properties:undefined

また、プロパティが存在してその値がundefinedなのか、それともプロパティが存在しないのかを見分けるためにはin演算子が使えます。
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/11_Expressions.html#section-11.8.7
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Operators:Special_Operators:in_Operator

m-hiyamam-hiyama 2005/12/10 12:48 山本さん、nanto_viさん、ご指摘ありがとうございます。
確かにIEでも、ふつうのオブジェクトに対してはhasOwnPropertyが動きますね。
undefinedは大域プロパティでしたか、僕が勘違いしていました。
in演算子は完全に見落としていました。
ご指摘を反映するように本文を直しました。おかげさまで改善され(より正確になり)ました。

malt3rdmalt3rd 2006/01/28 18:03 hasOwnProperty は Object.prototype のプロパティで、ECMA の仕様では
>グローバルオブジェクトの [[Prototype]] 及び [[Class]] プロパティの値は実装依存(http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/15-1_Global_Object.html)
なので、グローバルオブジェクトである window オブジェクトに hasOwnProperty がある事は保証できないということです。

m-hiyamam-hiyama 2006/01/28 18:17 malt3rdさん、ご指摘ありがとうございます。蒸し返し歓迎! です。
なるほど、windowオブジェクトは実行環境の一部ですから、ECMAでは規定できない対象と言えますね。とはいえ、ブラウザごとにマチマチってのもいやですなあ。

habukenhabuken 2007/05/29 09:54 分かりやすい解説ありがとうございます!

(function () { ... })() は、new function(){ ... } でも書き換えられるそうです。

■情報元
http://www.thinkit.co.jp/free/article/0703/10/5/

カッコ()の個数が削減できて見た目にも分かりやすく、
単純にコードブロック{}の冒頭にオマジナイとして
「new function()」を付けるだけでグローバル変数の汚染を防げるので、お手軽ですね。

m-hiyamam-hiyama 2007/05/29 10:42 habukenさん、
> (function () { ... })() は、new function(){ ... } でも書き換えられるそうです。
情報ありがとうございます。キータイプを少し節約できますね。