JavaScriptで複数のHTMLElementを取得し、その全てにイベントを設定しようとした場合
意識していないと案外ハマりそうな落とし穴を見つけたのでメモ。
個人的には理由に気づくまで「???」だったんですが、よくよく考えると当たり前の事実。。。

サンプルに以下のHTMLを用意してみました

簡単な5つの画像を表示するHTMLがあるとします。
ただし、すべてデフォルトではloading画像が表示され、本来表示する画像ではありません。

<html>
<body>
<h1>HTMLCollectionとイベント同時処理テスト</h1>
<div>
    <img id="after1" name="replace" src="loading.gif" />
    <img id="after2" name="replace" src="loading.gif" />
    <img id="after3" name="replace" src="loading.gif" />
    <img id="after4" name="replace" src="loading.gif" />
    <img id="after5" name="replace" src="loading.gif" />
</div>
</body>
</html>

そこでJavaScriptからdiv要素の中にあるloading画像を本来の画像に差し替えます。
ここでは分かりやすいように、各ノードのid名と同じ.gifファイルがあるとします。
※かなり設定に無理がありますが大目に見てください。。。

var oldImgs = document.getElementsByName("replace");

for(var i=0,j=oldImgs.length; i<j; i++)
    replace(oldImgs[i]);

function replace(oldImg){
    var newImg = new Image();
    newImg.onload = function(){
        oldImg.parentNode.replaceChild(newImg, oldImg);
    };
    newImg.src = oldImg.id + ".gif";
}

この程度のものだったら特にエラー等は発生しませんが、
上記のコードでは差し替える画像の数が膨大であった場合、ブラウザによっては
任意のノードがreplaceChildされるタイミングでエラーになることがあります。
自分の場合はIE6で複数の外部サイトに置かれた画像20枚に差し替えたとき発生しました。
また、リロード(キャッシュクリア含む)では発生せず、
リンクからページを表示させた場合に発生するというものでした。
つまり、文法的には誤りがあるわけではなさそうです。

デバッグしていくと、どうやら引数で受け取るoldImg(差し替え対象のノード)に
ときたま「undefined」が混じっているようでした。
なぜにundefined?getElementsByTagNameで取り損ね発生?
いや、仮にそうだとしたら既に致命的なバグがあるとお祭り騒ぎだし、
実際にgetElementsByTagName単体で実行した場合、取りこぼしがない。。。
エラー原因となったreplaceChildを実行させなかった場合もundefinedが渡らない。

何が起きた?

ブラウザが搭載しているJavaScriptエンジンの速度差が関係していました。
上記サンプルで注目すべきは「onload」イベントで「ノード置換していた」事です。
この置換のタイミングが画像の取得完了をトリガーにしていて
場合によっては、メイン処理完了を待たずしてイベントが発生するケースがあります。

現在は激しいブラウザ戦争により日々JavaScriptの高速化が進み、
もしイベントを処理の前段で定義し、複雑なメイン処理がその後にあっても
イベント発生前にすべての処理が終わっていることが多いです。
しかし、IE6のような遅いエンジンでは、処理量やイベントの発生条件を定義した場所や内容によっては
順序が前後するケースが可能性として出てきます。

加えて、イベントから呼ばれる処理の中でDOM操作がある場合
メイン処理上にイベント内で操作されるノードもしくは関係の深いノードを
直接参照する記述にしていると、モロにDOM操作結果の影響を受けます。
上記サンプルの場合はoldImgsを直接のループ対象として取り扱っていた点がそれに当たります。
oldImgsの要素ノードがreplaceされたことで、同じnameを持つHTMLCollectionの数が
途中で変化したことになり、突如ループ中の添字に対応する要素ノードが合わなくなったわけです。

解決方法は至って簡単

要素が変化する恐れのある、いわゆる「動的な」ノードリストは直接処理対象としないことです。
つまり、影響を一切受けない「静的な」ノードリストに退避させ
添字に対応するノードの存在を担保することで確実に欲しいノードを手に入れることができます。
静的なノードリストとしての退避先としてはArrayが最適です。

一番簡単な方法は、テンポラリーなArrayを用意し、それを参照対象とすることです。

var oldImgs = document.getElementsByName("replace"),
    i, j = oldImgs.length, tmpImgs = [];

for(i=0; i<j; i++)
    tmpImgs[i] = oldImgs[i];

for(i=0; i<j; i++)
    replace(tmpImgs[i]);

function replace(oldImg){
    var newImg = new Image();
    newImg.onload = function(){
        oldImg.parentNode.replaceChild(newImg, oldImg);
    };
    newImg.src = oldImg.id + ".gif";
}

また、広く知られたJavaScriptのイディオムを使う方法もあります。
Arrayクラスのsliceメソッドが、引数省略時にまんまArrayにして返却する特性を利用する方法です。
JavaScript1.6以降であれば「Array.slice」のみで実行することができますが
それ以前のバージョンサポートを考慮し、従来の「Array.prototype.slice.call(またはapply)」
を使用したほうがいいです。この記法のメリットはワンライナーで書けることですが、
IEではエラーとなるため(厳密にはargumentsには使用でき、DOMノードリスト系は使用できません)
使用には注意が必要です。。。

var oldImgs = Array.prototype.slice.call(document.getElementsByName("replace"));

for(var i=0,j=oldImgs.length; i<j; i++)
    replace(oldImgs[i]);

function replace(oldImg){
    var newImg = new Image();
    newImg.onload = function(){
        oldImg.parentNode.replaceChild(newImg, oldImg);
    };
    newImg.src = oldImg.id + ".gif";
}

結論

動的なノードリストに関わる機会としては以下のケースが多いと思います。

  • getElementsByNameを使う → HTMLCollectionが返る
  • getElementsByTagnameを使う → HTMLCollectionが返る
  • childNodesを使う → NodeListが返る

もし実装途中に「これはもしかして?」と思ったときは、常時instanceofで確認し、
「HTMLCollection」と「NodeList」のどちらかに該当したら迷わずArrayに退避させて(変換して)
そちらをベースに処理するクセを身につけておくのがオススメです。