配列を回しながら削除したい場合


ときおり、「配列に何かの処理をかけてから、1つ1つ削除していく」ような処理が必要となるかもしれません。ただ、うまくやらないとハマります

背景事情

Railsのフロントエンドの一部に、エレメントとしてRiotをはめ込んだようなアプリを作っていて、そしてTurbolinksでのページ遷移もかけていました。そうなると、ページ遷移の前にRiotをきちんと外しておく必要がありますので、以下のようなコードを書いていました。

幸い、マウント中のRiotタグの一覧はriot.util.vdomに配列として入っています。ということで、

$(document).on('turbolinks:before-cache turbolinks:before-render', function(){
  riot.util.vdom.forEach(function(item){
    item.unmount(true);
  });
});

のようにしていたのですが…調べてみると、一部のタグが残ってしまっていたのでした。

原因究明

Riot自体のソースコードまで当たって調べてみると、タグを.unmountしたのと同時にriot.util.vdomからも.splice()で削るようになっていました。そのため、

  • 0番目のタグを.unmount
  • .splice()で、もともと1番目に入っていたものが0番目に移る
  • forEachが1番目に進んでしまう
  • もと奇数番目だったものは、飛ばされて残ってしまう

という、単純な理屈でした。なお、JavaScriptの.forEachも、中身はこのように添字を回すのが基本です。

対策法

対策としては、いくつか考えられます。

後ろから回す

後ろから回していけば、順々に削除していっても番号がずれることはないので、そのような問題は生じません。Rubyの配列にはreverse_eachメソッドがありますが、JavaScriptでは添字をforで回していく他なさそうです。

配列をコピーする

操作対象となる配列とは別に、参照用に配列をコピーしておいて、そちらから操作する、という方法もあります。ただし、コピーのコストはかかります。

破壊的操作と要素の取り出しを一気に済ませる

配列から.pop().unshift()で要素を取り出しつつ削っていくようにできれば、「ループの位置」という概念も関係なくなりますし、全部終われば配列は自然と空になっています。