素人がプログラミングを勉強していたブログ

プログラミング、セキュリティ、英語、Webなどのブログ since 2008

連絡先: すかいぷ:javascripter_  か javascripter あっと tsukkun.net skypeのほうがいいです

array.indexOf(value) >= 0で要素が含まれてるか検索すると失敗する場合がある

indexOf(val) >= 0の話

配列に要素が含まれているかのチェックに

[1, 2, 3].indexOf(2) >= 0; // true

のようなコードを使っている場面を極めて頻繁に(それ以外を使っているのを見ることは稀なくらい)見るが、

function contains(a, v) {
  return a.indexOf(v) >= 0;
}

は厳密にはJavaScriptでは正しく動くとは限らない。上記コードのどこが間違っているのか、下記の解説を見る前に考えてみてほしい。こんなシンプルなコードにも、バグがある。

解説

containsがどのように動くべきなど明白だというのは間違いである。実は微妙な問題がたくさんある。 まず、致命的な部分をあげると、最初のナイーブな実装だと

contains([NaN], NaN) // false

となってしまう。明らかに意図した動作と異なり、バグである。 これは

NaN === NaN // false

という意味不明なJavaScriptNaNの仕様によるもので、indexOfも同様に===を使用しているので、こうなる。 訂正: コメントで指摘されたが、NaN !== NaNはほぼ全ての言語で採用されているIEEEの数値処理の仕様に基づいている(つまり不可解な仕様ではなくごく一般的な動作な)だけで、JavaScript自体の仕様上のミスではなかった。NaNまわりはハマりやすいポイントではあるので、気をつけたい。 contains関数では

contains([NaN], NaN]) // true

となるべきだ。

他にも、微妙な問題がある。 例えば、

[ ,,, ] [0] === undefined // true

であるが

0 in [ ,,, ] // false

であり、JavaScriptには、length以下に、holeと呼ばれる存在しないindexが存在する可能性があって、

[,,,].forEach(function () {alert(1); })

ではalertは一度も呼ばれない。indexOfも同様に存在しない要素をスキップするので

contains([,,,], undefined) // false

となる。これを仕様と呼ぶのかバグと呼ぶのかは考え方次第であるが、とりあえずforEachやindexOfにあわせて、holeは検索に含めないことにする。 ただし注意点として、最近はarrayのfor-inは順序が保証されているようだが

for (var i in a) {
  if (a.hasOwnProperty(i)) {

  }
}

は使ってはいけない。 なぜかというと

var a = [];
a['z'] =1;

の'z'までループしてしまうからだ。 また

for (var i = 0; i < a.length; i++) {
  for (i in a) {}
}

も同様にダメで、なぜかというと

Array.prototype[0] = 'a';

があると動作がおかしくなるからだ。というか

Array.prototype[0] = 'a';
var a = [, ];
a.indexOf('a') // 0
[,].forEach(function () { alert(1); }) // alert(1)される

のようにforEachとindexOfはプロトタイプチェーンを辿るので、この部分も難しいところであるが、今回はcontainsではプロトタイプチェーンを辿らないように実装した。

もう一点、これは余談だが、扱いの微妙な値がJavaScriptにはある。+0と-0である。一見同じようにみえる値だが、両者は厳密には異なる。

1/0 // Infinity

である一方、

1/-0 // -Infinity

であるからだ。

[-0].indexOf(+0) // 0

であるので、ここでは慣例に従って-0と+0はcontainsでは区別しないことにする。

さて、indexOfでは検索しはじめる位置を第3引数で指定できる。

[0].indexOf(0, 1) // false

となる。 もしこれに-Infinityを指定したらどうだろうか。

[0].indexOf(0, -Infinity) // 0

となり、きちんと、一瞬で答えが返ってくる。これも実装すべきだろう。

さて、おおよそ全ての値で動く実装を下記にあげる。

function contains(array, value, startIndex) {
  var hasOwnProperty = Object.prototype.hasOwnProperty;

  if (startIndex > array.length) { // for efficiency and also handling Infinity
    return false;
  } else if (startIndex === -Infinity) {
    startIndex = 0;
  } else {
    // when startIndex isn't specified make it 0
    // if startIndex is an invalid value, just make it 0 to behave like indexOf
    startIndex = +startIndex || 0;
  }

  for (var i = startIndex; i < array.length; i++) {
    // when array is sparse and value is undefined, ignore non-existent indices
    // 0 in [, ] returns true when Array.prototype[0] = 3
    // Therefore, hasOwnProperty must be used
    if (hasOwnProperty.call(array, i)) {
      var item = array[i];
      
      // when item is NaN and value isNaN
      if (item !== item && item !== value) {
        return true;
      } else if (value === item) {
        return true;
      }
    }
  }
  return false;
}

実行結果。

// regular values
contains([0, 1, 2], 0) // true
contains([0, 1, 2, 3], 0, 1 ) // false

// irregular values
contains([0], '0') // false
contains([NaN], NaN) // true
contains([,,], undefined) // false
contains([-0], +0) // true

contains([0], 0, -Infinity) // true
contains([0], 0, Infinity) // false
contains([0], 0, NaN) // true

ここまで読んで分かったと思うが、array.indexOf(val) >= 0が常に動かないことへ対処は、真面目にやろうとすると長い。長い上に他のJSのネイティブのAPIの仕様もちょっとずつおかしいので、プログラムを書く時点でNaNやInfinity, 穴のある配列を作らないように作るのがベターである。