ループを使ってjavascriptの配列からspliceで要素を削除するときの注意点

2016年11月7日(更新: 2016年11月7日)

javascriptで配列の要素を取り除くときの話です。

spliceについて

JavaScriptにおいて、配列から要素を削除するときは splice というメソッドを使うことができます。

Array.prototype.splice() – JavaScript | MDN

指定したインデックス番号から複数の要素を取り除いたり、要素の除去と同時に新しく要素を挿入したりできます。

取り除く要素数を1にすることで、配列の途中にある指定した要素だけを取り除くといった使い方ができます。

spliceの使用例

要素を1つ削除する

今回テーマとする、配列から要素を1つ取り除く例を以下に示します。

(function() {
  var array = ['a', 'b', 'c', 'd', 'e'];
  array.splice(2, 1);

  console.log(array); // ['a', 'b', 'd', 'e']
}());

上のプログラムは、配列arrayからインデックス番号が2(つまり配列の3個目)の要素である「c」を削除します。配列のインデックス番号は 0 から始まることに注意してください。

要素を複数削除する

複数の要素を削除したい場合は、第2引数に削除したい個数を入力します。

次のプログラムは、配列の2〜4番目(インデックス番号で言えば1〜3)の要素を削除します。

(function() {
  var array = ['a', 'b', 'c', 'd', 'e'];
  array.splice(1, 3);

  console.log(array); // ['a', 'e']
}());

削除した要素の情報を取得する

splice は戻り値があり、取り除いた要素の配列が返されます。

例えば、配列のインデックス番号が2(つまり配列の3個目)の要素から2つの要素を取り出した際、取り出したものの情報を取得したいのであれば、以下のように引数を受け取ります。

(function() {
  var array = ['a', 'b', 'c', 'd', 'e'];
  var removed = array.splice(2, 2);

  console.log(array); // 取り除いた後の配列 ['a', 'b', 'e']
  console.log(removed); // 取り除かれた要素 ['c', 'd']
}());

ループ時に要素を削除する

Cocos2d-js で、スプライト同士の衝突判定を作ったときのことです。

「forループで敵オブジェクトを格納している配列の全要素にアクセス(イテレート)して、プレイヤーと衝突している要素が見つかったら削除する」という処理を書こうとして、次のようなループのプログラムを書きました。

//変数の説明
/***************************************
enemys : 敵オブジェクトを格納した配列
playerHitRect : プレイヤーの衝突判定領域
i : インデックス番号
***************************************/

// 敵を格納した配列の要素数だけループ
for (var i = 0; i < enemys.length; i++) {

  // 2つの矩形の重なりをチェックする
  if (cc.rectIntersectsRect(enemys[i].getBoundingBox(), playerHitRect)) {

    // 衝突した敵を削除
    enemys.splice(i, 1);
  }
}

もしi番目の敵とプレイヤーが衝突したら、i番目の敵を取り除くという処理です。しかし、この方法には問題があります。

問題

ループの途中で要素を削除してしまうと、インデックス番号にズレが生じてしまい、正しく全要素にアクセスできなくなります

spliceによって配列要素の数が変わっても enemys.length は古いままです。

例えば、もともとの要素数が10個だった場合、要素を削除して9個になったとしても、このループは要素が10個あるものとして10個目の要素にアクセスしようとしてしまいます。

これが起こると、最悪の場合、存在しない要素にアクセスしようとしてプログラムが停止します。

対策

インデックス番号をインクリメント(増やす)ではなくデクリメント(減らす)にすると、取り除いた要素より前のインデックス番号はずれないため、要素数が減っても問題ありません。

// 敵を格納した配列の要素数だけループ
for (var i = enemys.length - 1; i >= 0; i--) {

  // 2つの矩形の重なりをチェックする場合は cc.rectIntersectsRect を使う
  if (cc.rectIntersectsRect(enemyHitRect, playerHitRect)) {

    // 衝突した敵を削除
    enemys.splice(i, 1);
  }
}

whileループを使うと次のように書けます。

// 敵を格納した配列の要素数だけループ
var i = enemys.length;

while(i--) {

  // 2つの矩形の重なりをチェックする場合は cc.rectIntersectsRect を使う
  if (cc.rectIntersectsRect(enemyHitRect, playerHitRect)) {

    // 衝突した敵を削除
    enemys.splice(i, 1);
  }
}

以上、ループ内でspliceを使う際の注意点でした。

コメントを残す

メールアドレスが公開されることはありません。