51
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScriptでの「空」の値についてのおさらい

Last updated at Posted at 2017-10-13

 久々のおさらいシリーズ。

 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 さんの下記の記事が抜群にわかりやすい。

isset, empty, is_null の動作まとめ

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() 関数の解釈に準じる。
  • シンボル型や関数オブジェクト型は、一律で「空」とはしない。

──これらの「空」判定ルールに沿って作った関数がこれである。

is_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などのオブジェクトも存在するため、それらの「空」判定も行うとなるとその分関数処理が肥大化していくことになるだろう。
 
 結果的には、実用に耐えられそうな「空」判定関数が作成できたので、収穫は大きかったと云える。 

51
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?