Hatena::ブログ(Diary)

風と宇宙とプログラム このページをアンテナに追加 RSSフィード

2010-05-12

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のみであり、逆も同じである。

おわりに

undefinedには多義的に使われており、なかなかのクセ者である。ブール値や数値などとの比較はC/C++言語などに慣れたプログラマの直感とは合わない。僕なんかも時々「あれ?どうだったっけ?」と迷ってしまうことがある。そういう意味で自分の記憶に焼き付けるという効果も含めてエントリとして書いてみたが、何かの参考になれば幸いである。

gotingotin 2010/05/13 17:07 プロパティの値がundefinedなのか、あるいは、プロパティが定義されていないのかを区別する手段としては、
"<property name>" in target_object
を使うとよい気がしますがいかがでしょうか。
prototypeチェーンも追って判定してくれます。
trueならオブジェクトのプロパティとして存在してます。

mindcatmindcat 2010/05/14 02:29 gotinさん、
ご指摘ありがとうございます。全くその通りです。大ボケしてしまいました。prototypeチェーンをユーザプログラムで辿れるかが念頭にあったためinオペレータに気が回らなかったようです。修正しておきました。

gotingotin 2010/05/14 11:06 なるほどなるほど。
ちなみに、ご存知かもしれませんが、prototypeチェーンをたどるための仕組みはJavaScriptの仕様上は定義されていないと思いますが、いくつかの実装ではユーザプログラムからprototypeオブジェクトを参照できるようになっているものもあるようです。例えば今firefox(version3.6.4 for MacOSX)のfirebug(version1.5.4)のコンソール上で試したところtarget_object.__proto__ でprototypeオブジェクトを参照できました。__proto__を活用すべき場面があるのかどうかはよくわからないのですが、複雑なprototypeチェーンを扱うようなプログラムの開発時のデバッグには役立ちそうですね。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証