久々のおさらいシリーズ。
JavaScriptにはオブジェクトや配列などの引数に与えた変数(値)が「空」かどうかを判定する関数がない。つまり、PHPで云うところの empty()
や、Railsで云うところの blank?
といったビルトインメソッドがないのだ。
ある変数について値が空かどうかという条件式は結構多用するので、ないならば作ってしまおう、ということでPHPの empty()
的な関数を自作してみた(実際のソースは後述)。その際、JavaScriptでの様々な「型」における「空」の判定について調査して、色々と気づいたことも多かったので、この記事にまとめてみた次第。
JavaScriptにおける「型」別の「空」について
まず、JavaScriptにおいて変数が「空」であるという定義にはブレがある、というか一意な解釈がないという方が正しいのかもしれない。それが「空」を判定するようなビルトインメソッドがないことに繋がっているのだろう。変数型ごとの「空」の解釈が利用者に依存しているような状況なうえ、さらにECMAScript6で追加された「シンボル」型がその定義をさらに混沌とさせている(ような気がする)。
理論だけだといまいちピンとこないので、有名どころのライブラリである「is.js」と「underscore.js」を引き合いにして、実際に値の「空」がどのように判定されているかを例示してみよう。
判定する値 | 値の説明 | is.js: is.empty() | underscore.js: _.isEmpty() | 期待値としての判定結果 |
---|---|---|---|---|
{} |
空のオブジェクト | true | true | true |
sym = Symbol(); Object(sym) |
プリミティブ値がシンボルのオブジェクト | true | true | プリミティブ値が存在しているオブジェクトなので false であるべき |
obj = {}; a = Symbol('a'); obj[a] = 'foo' |
シンボルのプロパティを持つオブジェクト | true | true | プロパティが存在するので false であるべき |
{ constant: [], method: function(){} } |
メソッドを持つクラスオブジェクト | false | false | false |
Symbol() |
シンボル | false | true | 暗黙的にプリミティブデータをラップしているため false であるべき |
Symbol("Foo") |
descriptionを持つシンボル | false | true | 同上 |
Symbol.for("foo") |
キーを持つシンボル | false | true | 同上 |
new String() |
空のStringオブジェクト | false | true | 値がないオブジェクトのため true であるべき |
new String("Bar") |
プリミティブ値を持つStringオブジェクト | false | false | 値を持つオブジェクトのため false であるべき |
"bar" |
文字列 | false | false | false |
"" |
空の文字列 | true | true | true |
"0" |
文字列の0 | false | false | 文字列として値があるので false であるべき |
new Number() |
空のNumberオブジェクト | true | true | 値がないオブジェクトのため true であるべき |
new Number(0) |
プリミティブ値が0のNumberオブジェクト | true | true | 数値の0として判定し true が理想 |
new Number(1) |
プリミティブ値が1のNumberオブジェクト | true | true | 数値の1として判定し false が理想 |
1 |
数値の1 | false | true | 真偽値解釈として0以外の数値は false が理想 |
-1 |
数値の―1 | false | true | 同上 |
0 |
数値の0 | false | true | 真偽値解釈として0はfalseと同等に扱い true が理想 |
3.14 |
(浮動小数点を含む)数値の3.14 | false | true | false |
NaN |
非数のグローバルプロパティ | false | true | 数値でない値が存在するという意味から false が理想 |
[] |
空の配列 | true | true | true |
[1,2,3] |
配列 | false | false | false |
arr = [a,b,c]; arr[Symbol.iterator] |
ウェルノウンシンボルのイテレータ | false | true | 実体はfunction型であるため false であるべき |
new Boolean() |
空のBooleanオブジェクト | true | true | 値がないオブジェクトのため true であるべき |
new Boolean(true) |
プリミティブ値がtrueのBooleanオブジェクト | true | true | 真偽値のtrueとして判定し false が理想 |
new Boolean(false) |
プリミティブ値がfalseのBooleanオブジェクト | true | true | 真偽値のfalseとして判定し true が理想 |
true |
真偽値のtrue | false | true | 真偽値解釈として有効な値という判定から false が理想 |
false |
真偽値のfalse | false | true | 真偽値解釈として無効な値という判定から true が理想 |
undefined |
undefiend型オブジェクトのプリミティブ値 | false | true | 値が未定義なので true であるべき |
null |
null値(リテラル) | false | true | 値がないので true であるべき |
期待値についての解釈には個人差があるかもしれない。それこそが、JavaScriptにおける「空」判定の難しさであり、混沌の深淵でもある。私が期待値として解釈のベースとしたのは、PHPの empty()
関数である。そのPHPの empty()
による判定結果は下記の通りだ。
判定する値 | 型 | 値の説明 | PHP: empty() | 補足 |
---|---|---|---|---|
new stdClass() |
object(stdClass) | 空のオブジェクト | false | オブジェクトの空判定には get_object_vars などを使うべき |
(object) "ciao" |
object(stdClass) | スカラー値のみを持つオブジェクト | false | 同上 |
new Class{} |
object(class@anonymous) | 空のクラス | false | 同上 |
$closure = function(){ return; } |
object(Closure) | クロージャ | false | 同上 |
"foo" |
string(3) | 文字列 | false | |
"" |
string(0) | 空の文字列 | true | |
"0" |
string(1) | 文字列の0 | true | 文字列として値があるので false であるべき |
1 |
int(1) | 整数値 | false | |
-1 |
int(-1) | 整数値の-1 | false | |
0 |
int(0) | 整数値の0 | true | |
0.0 |
float(0) | 浮動小数点値の0 | true | |
array() |
array(0) | 空の配列 | true | |
array(1,2,3) |
array(3) | 配列 | false | |
true |
bool(true) | 真偽値のtrue | false | |
false |
bool(false) | 真偽値のfalse | true | |
global $glob_var |
NULL | 定義済みグローバル変数 | true | 対象スコープ上に変数が定義されたのみで値はない |
static $stat_var |
NULL | 定義済みローカル変数 | true | 同上 |
null |
NULL | null値 | true | |
new SimpleXMLElement('<div></div>') |
object(SimpleXMLElement) | 空要素のみのSimpleXMLElementオブジェクト | true | |
new SimpleXMLElement('<div class="clearfix"></div>') |
object(SimpleXMLElement) | 属性あり空要素のSimpleXMLElementオブジェクト | false | |
tmpfile() |
resource of type (stream) | ストリーム参照リソース(いわゆるファイルハンドル) | false | リソースの空判定には別途 fread などで実データを取得する必要がある |
$fh = fopen("//google.com", "r"); fclose( $fh ) |
resource of type (Unknown) | 参照を破棄されたリソース | false | リソースの開放を判定するには is_resource を使う |
何気に、PHPの「空」判定も十分にカオスなのだが、判定指針がユーザー任せのJavaScriptよりはだいぶマシであり、参考にできるのではないかと。
PHPの「空」判定については、 @mpyw さんの下記の記事が抜群にわかりやすい。
JavaScript の typeof には罠がある
前述した通り、JavaScriptで「空」判定を行うにあたっては変数の「型」の識別が重要になってくる。まず、変数の型を判定して、それぞれの型に沿って値が「空」かどうかを調べるという手順になるからだ。
そこで、JavaScriptにおける型について簡単におさらいしておこう。@amamamaou さんや、 @south37 さんの記事が非常に参考になる。
上記の記事で云われているように、JavaScriptにおけるオブジェクト型には内包される要素が非常に多い。さらに、ECMAScript6から新たに追加された シンボル型 にも注意が必要になる。
──ということを踏まえて、JavaScriptでの型を一覧化してみると次のようになる。
型 | 型の概要 | typeof の結果 |
---|---|---|
String | 「文字列」型 | string |
Number | 「数値」型。非数である NaN もこの型に属する |
number |
Boolean | 「真偽値」型 | boolean |
Undefined | 「未定義」型 | undefined |
Null | 本来は「Null」型に分類されるべきところだが、実際にはオブジェクト型と判定される。ECMAScriptのバグと云われている | object |
Symbol | 「シンボル」型。ECMAScript6 で新規追加された | symbol |
Function | 「関数オブジェクト」型 | function |
XML | 「E4X XML オブジェクト」型。私は今までお目にかかったことがない稀少な型 | xml |
Object | 「オブジェクト」型。前出の型以外はすべてこの型となる | object |
JavaScriptで型を判定する際に使われる typeof
に依存してしまうと、Nullがオブジェクト型と認識されてしまい、値の「空」判定においては致命的になりかねない。また、プリミティブ値として文字列や数値、真偽値を持ちながらオブジェクトとして判定される new String("bar")
のようなラッパーオブジェクトにも注意が必要だ。
──では、注意点をおさえながら、実際に「空」判定のコードを書いてみる。
値の「空」判定関数
判定処理の期待値として、私が考える値の「空」の解釈の特筆すべき点は下記の通りだ。
- オブジェクト型については、プロパティがシンボルなどのプリミティブ値であっても、それを値とみなして「空」とはしない。
- ラッパーオブジェクトのオブジェクト型は、変換されるプリミティブ値にもとづいて「空」判定を行う。
- ネストするオブジェクトについては最終的なプリミティブ値まで再帰的に「空」判定を行う。
- 真偽値型の「空」判定はPHPの
empty()
関数の解釈に準じる。 - シンボル型や関数オブジェクト型は、一律で「空」とはしない。
──これらの「空」判定ルールに沿って作った関数がこれである。
var is_empty = function( _var ) {
if ( _var == null ) {
// typeof null -> object : for hack a bug of ECMAScript
return true;
}
switch( typeof _var ) {
case 'object':
if ( Array.isArray( _var ) ) {
// When object is array:
return ( _var.length === 0 );
} else {
// When object is not array:
if ( Object.keys( _var ).length > 0 || Object.getOwnPropertySymbols(_var).length > 0 ) {
return false;
} else
if ( _var.valueOf().length !== undefined ) {
return ( _var.valueOf().length === 0 );
} else
if ( typeof _var.valueOf() !== 'object' ) {
return is_empty( _var.valueOf() );
} else {
return true;
}
}
case 'string':
return ( _var === '' );
case 'number':
return ( _var == 0 );
case 'boolean':
return ! _var;
case 'undefined':
case 'null':
return true;
case 'symbol': // Since ECMAScript6
case 'function':
default:
return false;
}
}; // End of is_empty()
この例では「is_empty」を無名関数として定義しているが、この場合、関数の巻き上げが行われないため、実際に「空」判定を行いたい処理より前にコードを書いておかないと実行できない。JavaScriptの読み込み順序などを気にせずに使いたい場合は、通常の関数宣言として記述しておくとよいだろう。
判定結果
判定する値 | 値の説明 | is.js: is.empty() | underscore.js: _.isEmpty() | is_empty() |
---|---|---|---|---|
{} |
空のオブジェクト | true | true | true |
sym = Symbol(); Object(sym) |
プリミティブ値がシンボルのオブジェクト | true | true | false |
obj = {}; a = Symbol('a'); obj[a] = 'foo' |
シンボルのプロパティを持つオブジェクト | true | true | false |
{ constant: [], method: function(){} } |
メソッドを持つクラスオブジェクト | false | false | false |
Symbol() |
シンボル | false | true | false |
Symbol("Foo") |
descriptionを持つシンボル | false | true | false |
Symbol.for("foo") |
キーを持つシンボル | false | true | false |
new String() |
空のStringオブジェクト | false | true | true |
new String("Bar") |
プリミティブ値を持つStringオブジェクト | false | false | false |
"bar" |
文字列 | false | false | false |
"" |
空の文字列 | true | true | true |
"0" |
文字列の0 | false | false | false |
new Number() |
空のNumberオブジェクト | true | true | true |
new Number(0) |
プリミティブ値が0のNumberオブジェクト | true | true | true |
new Number(1) |
プリミティブ値が1のNumberオブジェクト | true | true | false |
1 |
数値の1 | false | true | false |
-1 |
数値の―1 | false | true | false |
0 |
数値の0 | false | true | true |
3.14 |
(浮動小数点を含む)数値の3.14 | false | true | false |
NaN |
非数のグローバルプロパティ | false | true | false |
[] |
空の配列 | true | true | true |
[1,2,3] |
配列 | false | false | false |
arr = [a,b,c]; arr[Symbol.iterator] |
ウェルノウンシンボルのイテレータ | false | true | false |
new Boolean() |
空のBooleanオブジェクト | true | true | true |
new Boolean(true) |
プリミティブ値がtrueのBooleanオブジェクト | true | true | false |
new Boolean(false) |
プリミティブ値がfalseのBooleanオブジェクト | true | true | true |
true |
真偽値のtrue | false | true | false |
false |
真偽値のfalse | false | true | true |
undefined |
undefiend型オブジェクトのプリミティブ値 | false | true | true |
null |
null値(リテラル) | false | true | true |
期待値通りの判定が行われている。
参考までに、判定結果のテスト用コードも紹介しておく。
// TEST
var lib = [ "is", "underscore", "my_func" ],
useLib = lib[2], // Which library to test
obj = {},
sym = Symbol(),
a = Symbol('a'),
arr = [1,2,3],
obj2 = { constant: [], init: function(){}, method: function(){} };
obj[a] = 'foo';
var _chk = [ {}, Object(sym), obj, obj2, sym, Symbol("Foo"), Symbol.for("foo"), new String(), new String("Bar"), "bar", "", "0", new Number(), new Number(0), new Number(1), 1, -1, 0, 3.14, NaN, [], arr, arr[Symbol.iterator], new Boolean(), new Boolean(true), new Boolean(false), true, false, undefined, null ];
_chk.forEach(function( v ){
var res, use;
if ( useLib === "my_func" ) {
use = "is_empty()";
res = is_empty( v );
} else
if ( useLib === "is" ) {
use = "is.js:is.empty()";
res = is.empty( v );
} else
if ( useLib === "underscore" ) {
use = "underscore.js:_.isEmpty()";
res = _.isEmpty( v );
}
console.log({ lib: use, arg: v, typeOf: typeof v, result: res });
});
まとめ
JavaScriptにおける値の「空」判定は、結局のところ各ユーザーの「空」の解釈によってブレがあるため、横断的に変数が「空」かどうかを判定するビルトインメソッドのようなものがないのだろう。
それをユーザー関数でカバーしようとすると、JavaScriptの大雑把な「型」判定に悩まされることが分かった。特に、実際には様々な型を有するオブジェクト型をどこまで切り出して「空」判定をするのかというポリシー的なものが大きいのではないだろうか。
今回の「空」判定では、対象とするオブジェクト型を配列とオブジェクトとラッパーオブジェクトに限定してあるが、他にもDateやJSON、Error、RegExpなどのオブジェクトも存在するため、それらの「空」判定も行うとなるとその分関数処理が肥大化していくことになるだろう。
結果的には、実用に耐えられそうな「空」判定関数が作成できたので、収穫は大きかったと云える。