JavaScriptのundefinedというクセ者のいろいろ
はじめに
JavaScriptの初心者にとってundefinedというのはちょっと混乱を招くものらしい。nullとの違いや使い分けがよく分からなかったり、数値やブール値との比較が不自然だったりするのが原因と思われる。ここでは、そのようなundefinedのいろいろについてまとめてみた。
ECMA262規格では
undefinedとnullに関して、ECMA262規格では最初に以下のような記述がある。
4.3.9 undefined value
primitive value used when a variable has not been assigned a value.
4.3.11 null value
primitive value that represents the intentional absence of any object value.
undefinedは変数に値がまだ代入されていないというときに使われ、nullはオブジェクトが存在しないとことの意図的な表現として説明されている。
確かにその通りであるが、これは極一部の説明にしかなっていない。以下、この記述では表現できない諸々のものについて列挙してみた。
変数の初期値としてのundefined
undefinedは変数に値がまだ代入されていない状態というより、変数を宣言すると明示的に初期化しなくてもundefinedで初期化される、と理解した方がよい。
var a; var b = undefined; var c = 123; c = undefined;
変数bはundefinedで明示的に初期化しているが、変数aとの実質的な違いはない。変数cは123で初期化しているが、その後にundefinedを代入しているので、これも限りなく変数aと近い状態である。つまり、undefinedは変数に値がまだ代入されていない状態を表すというより、undefinedは変数のデフォルトの値に過ぎず、その他の値と大きな違いは違いはないと考えてもよい。
このとき、変数a, b, cの値の型をtypeofで調べるといずれも"undefined"という文字列が返される。一方、var宣言もされておらず、変数としても存在していないようなものに対してtypeofを実行しても同様に"undefined"という文字列が返される。
var a; typeof a === "undefined"; // true typeof x === "undefined"; // true
これだけを見ると、var宣言した変数aと宣言も何もしていなく突然現れたxとの違いがあまりないように見えるが、そこには大きな違いがある。typeof a
の方は変数aの値がundefinedという値であり、そのundefinedという値に対してtypeof を実行すると"undefined"という文字列になる、ということである。一方、typeof x
の方は、スコープ中にxというものが存在せず、そのような例外的な状況の結果としてtypeofが"undefined"という文字列を返しているのである。
この両者の違いを簡単に区別できるかというと、グローバル変数あれば簡単に区別できる。グローバル変数はグローバルオブジェクト(ブラウザの場合はwindowオブジェクト)のプロパティであるので、hasOwnProperty()を実行すればよい。
var a; window.hasOwnProperty("a"); // true window.hasOwnProperty("x"); // false
しかし、ローカル変数の場合など、一般には両者の違いを区別するには、実際にxを評価してみてReferenceError例外が投げられるかどうかで判断するしかない。
try { x; } catch (e) { if (e instanceof ReferenceError) { alert("x: undefined variable"); } }
JavaScriptのプロパティに対して、Rubyのmethod_missing()のようなフォールバックを記述できる仕組みがあればどんなに便利だろうと思うことがあるが、残念ながらそのようなものはない。
プロパティの値としてのundefined
変数のvar宣言と異なって、オブジェクトのプロパティは明示的に宣言することができない。プロパティに明示的にundefinedという値が設定されているのか、あるいは、プロパティそのものが存在しないのかどうかは、hasOwnProperty()関数を利用すればよいが、prototypeチェーンを含めた場合には使えない。変数などと違って、存在しないプロパティを評価しても例外が投げられるわけではないので、一般にはプロパティの値がundefinedなのか、あるいは、プロパティが定義されていないのかを区別する手段はない。あるオブジェクトに対して、指定されたプロパティが定義されているかどうかはinオペレータを使えばよい。"propname" in obj
という形式であり、これはprototypeチェーンも考慮される。(修正追加:2010/5/13)
補足:protoypeチェーンはprototypeというプログラムでアクセスできるプロパティを利用しているのではなく、[[Prototype]]という内部のプロパティを利用しているため、ユーザプログラムでprototypeチェーンをたどることはできない。prototypeプロパティは変更できてしまうが、内部の[[Prototype]]が変更されることはない。
配列要素としてのundefined
下記のコードはどちらも要素数が5の配列を生成する。
var a1 = Array(5); // new Array(5)でも同じ var a2 = [,,,,,]; // 最後のコンマは無視されるので、ひとつ多いことに注意
どちらの配列も各要素はundefinedであるが、両者は等価ではない。配列のインデックスも通常のプロパティと同じであるが、Array関数で生成された配列は各プロパティがundefinedで初期化されるわけではない。一方、リテラル形式で初期化した配列は要素を省略した場合でも明示的にundefinedで初期化される。
つまり、以下のような違いがある。
a1.hasOwnProperty("3"); // false a2.hasOwnProperty("3"); // true
関数の戻り値としてのundefined
C言語では何も返さない関数はvoid型として定義できるが、JavaScriptの場合、何も返さない関数とundefinedを返す関数の区別がつかない。以下の3つの関数は基本的に同等である。
function fun1() { } function fun2() { return; } fuction fun3() { return undefined; } var v1 = func1(); // undefined var v2 = func2(); // undefined var v3 = func3(); // undefined
正常系の動作として数値を返す関数の仕様を定義する場合、異常系の動作のとき例外を投げるのがよいのかundefinedを返すのがよいのか、しばしば悩むことがある。undefinedを返すようにしたとき、コード上で明示的にreturn undefined
を書くか書かないかは好みの問題になってしまう。書かない場合には静的解析ツールなどで警告が検出されることもあるし、コードを読む人も不安になる。最低でもコメントなどでの注意書きが必要であろう。
関数の引数としてのundefined
関数の仮引数の数より実引数の数の方が少ない場合、余った引数にはundefinedがバインドされる。引数が足りないのか、あるいは、明示的にundefinedが指定されたのかを判断するには、arguments.length 使えばよい。
function func(a, b, c) { alert("len=" + arguments.length + ",a=" + a + ",b=" + b + ",c=" + c); } func(123); // len=1,a=123,b=undefined,c=undefined func(123, undefined); // len=2,a=123,b=undefined,c=undefined
ちなみに、仮引数の数関数オブジェクトのlengthというプロパティにセットされている。一般に引数の数を表す用語としてarityというのがあるが、Firefoxではarityというプロパティにも仮引数の数がセットされている。
var n = func.length; // n==3 var m = func.arity; // m==3 (Firefox)
数値としてのundefined
undefinedは数値ではないが、数値に変換することができる。
+undefined; // NaN Number(undefined); // NaN
通常、undefinedを数値に変換するとNaNになる。
NaNは浮動小数の一種だ。なので、NaNを整数に変換するとゼロとなるように定義されている。このため、undefinedを整数に変換するとNaNではなくゼロになることで辻褄が合う。
例えばビット演算は32bit整数に変換してから実際のビット演算が実行されるので、以下のようになる。逆に言えば、ビット演算の結果がNaNになることはない。
undefined | 1 // 1 undefined >> 3 // 0 3 >> undefined // 3 123 | "hello" // 123
ビット演算以外に任意のものを整数に変換する処理が登場するものとして、位置を表すものによく使われる。つまり、「先頭からN番目」というときなどのNは整数に変換される。その代表的なものに、StringのindexOf()やcharAt()などがある。
charAt()はちょっと注意が必要だ。文字列中の文字は配列のようにアクセスすることができるが、配列表現とcharAt()は同じではない。
var s = "hello"; s[1]; // 'e' s.charAt(1); // 'e' s[2.5]; // undefined s.charAt(2.5); // 'l' s[undefined]; // undefined s.charAt(undefined); // 'h' s.charAt(); // 'h'
charAt()は引数を整数に変換するが、配列表現のインデックスは整数に変換することはない。配列のインデックスは仕様的には文字列に変換される。
ちなみに、Infinityを整数に変換するときもゼロになる。
Booleanとしてのundefined
undefinedをBooleanであるtrueやfalseに対して比較(==)するとどちらもfalseになる。
undefined == true; // false undefined == false; // false
trueじゃなければfalseでしょ、と思いたくなるがそうはならない。==
は左右のどちらかがBooleanのときはそれを数値に変換して==
する。なので、上記は下記と同じである。
undefined == 1; // false undefined == 0; // false
つぎに、==
は左右のどちらかが数値でもう一方が数値でない基本型のときは、その値に関わらずfalseになる。従って、上の2つの式はどちらもfalseになる。
undefinedはBoolean型に変換するとfalseになる。一般に何かをBoolean型に変換するとtrue(またはfalse)になるけど、それ自身はtrue(またはfalse)と==
にならないことがある。
なので、
if (x == true) ... if (x == false) ...
という書き方はではなく、
if (x) ... if (!x) ...
と書くのが普通である。xがundefinedの場合が明らかなように、この両者のif文は等価ではない。
undefinedと==
なのはundefined自身とnullのみであり、逆も同じである。