for (in) 文の内部から走査中の(連想)配列の項目数をいじる
昨日のエントリにてPythonのfor in文について書きましたがJavascriptの場合はどうだったかな、と思って調べてみました。
ループ中にループ内から走査中の配列・連想配列の項目を削除、追加した場合の挙動についてです。
そのような処理を実際に書いたことはこれまでありませんでした。
まずfor文ですがこちらについてはある程度予想ができます。
for(var i = 0; i < array.length; i++){};
という書き方をしておけばiがインクリメントされるとその都度配列の長さと比較されるため内部で配列の長さが変化した場合も特にややこしい点はなさそうです。
処理中の項目とその時点の配列の長さを表示してみます。
var testArray = ["ゼロ", "イチ", "ニ", "サン", "シ", "ゴ", "ロク", "シチ"]; (function(){ for (var i = 0; i < testArray.length; i++) { //ループしながら document.write(testArray[i] + ":" + testArray.length + "<br>"); //項目とその時点の配列の長さを表示して testArray.splice(i,1); //その要素を削除 } })();
結果は
ゼロ:8 ニ:7 シ:6 ロク:5
i = 4 の時はすでに配列の長さが4になっているため実行されません。
予想通りの結果です。Pythonのfor inと同じ結果ではありますが処理の流れが見えるので分かりやすいです。
Javascriptの配列はPythonのリストと違って空の要素を持つこともできるためその場合も確認してみます。
var testArray = ["ゼロ", "イチ", "ニ", "サン", "シ", "ゴ", "ロク", "シチ"]; (function(){ for (var i = 0; i < testArray.length; i++) { document.write(testArray[i] + ":" + testArray.length + "<br>"); delete testArray[i]; } })();
結果は
ゼロ:8 イチ:8 ニ:8 サン:8 シ:8 ゴ:8 ロク:8 シチ:8
要素が空になるだけで前にずれないのでそのまま普通に表示した場合と同じになるだけでした。
配列の先頭に要素を追加すると無限ループになります。
var testArray = ["ゼロ", "イチ", "ニ", "サン", "シ", "ゴ", "ロク", "シチ"]; (function(){ for (var i = 0; i < testArray.length; i++) { document.write(testArray[i] + ":" + testArray.length + "<br>"); testArray.unshift("マイナス"); } })();
結果は
ゼロ:8 ゼロ:9 ゼロ:10 ゼロ:11 ゼロ:12 ゼロ:13 ゼロ:14 ゼロ:15 ゼロ:16 ゼロ:17 ゼロ:18 (無限ループ)
ちなみにIE8で5回試すとそれぞれ配列の長さが"6278","6318","6238","6262","6310"の所まで表示した段階で「このまま続けますか?」の警告が出ました。
どの段階で無限ループを警告するかというのは固定ではないようです。計算量ではなく時間での判定かもしれません。
次にfor in文を確認してみます。
Javascriptのfor in文はPythonのそれとはちがって連想配列(=オブジェクト)を走査します。
まず処理の終わった項目(プロパティ)を削除する場合。
var testHash = { first : "one", second : "two", third : "three", fourth : "four", fifth : "five", sixth : "six", seventh : "seven" }; (function(){ for (var key in testHash) { //連想配列をループして document.write(testHash[key] + "<br>"); //表示した後 delete testHash[key]; //表示した項目を削除 } var count = 0; for (var key2 in testHash) { count++ } document.write(count); //残っているプロパティの数(連想配列の長さ)を表示 })();
これを実行すると結果は
one two three four five six seven 0
処理済みのプロパティを削除するのは特に問題ないようです。
ちなみにこの結果では"one","two"...と記述した通りの順番に表示されましたが、for in文が必ずプロパティを記述順に処理するとは限りません。
次に、表示したプロパティとは異なるプロパティ=未処理のプロパティを削除してみます。
下のコードはキーが"a"のプロパティが処理されたら"aa"、"aa"が処理された場合は"aaa"というようにaの数が1つ多いプロパティを削除しています。"aaaaaaa"が処理された場合は"a"を削除します。
var testHash = { a : "one", aa : "two", aaa : "three", aaaa : "four", aaaaa : "five", aaaaaa : "six", aaaaaaa : "seven" }; (function(){ var pToDelete; //削除するプロパティ for (var key in testHash) { //連想配列をループして document.write(testHash[key] + "<br>"); //表示した後 pToDelete = (key == "aaaaaaa") ? "a" : key + "a"; delete testHash[pToDelete]; //表示した項目より"a"が1つ多い項目を削除 } var count = 0; for (var key2 in testHash) { count++ } document.write(count); //残っているプロパティの数(連想配列の長さ)を表示 })();
結果は
one three five seven 3
エラーはなく↑の結果でした。
つまり削除しても別に問題は生じないようです。for in文は処理を始める時点でオブジェクトがどういう状態であるかはチェックしないみたいですね。
次にfor in文のループ中にプロパティを追加する場合をチェックしてみます。
次のコードはfor in文に入る時点で"a"というプロパティのみのオブジェクトに、処理したプロパティ名+"a"というプロパティを追加していっています。
var testHash = { a : "one" }; (function(){ for (var key in testHash) { //連想配列をループして document.write(key + ":" + testHash[key] + "<br>"); //キー名と値を表示した後 testHash[key + "a"] = "any"; //処理したプロパティに"a"をたしたプロパティを追加。値は"any" } var count = 0; for (var key2 in testHash) { count++ } document.write(count); //残っているプロパティの数(連想配列の長さ)を表示 })();
結果は
a:one aa:any aaa:any aaaa:any aaaaa:any aaaaaa:any aaaaaaa:any aaaaaaaa:any aaaaaaaaa:any (以下無限ループ)
ループ内で追加されたプロパティは未処理のプロパティと判断されるようです。
つまりfor in文は処理済のプロパティのみを覚えていて、未処理のプロパティが存在するかどうかはループの最後に毎回判断しているようです。
まとめ
for文は見た目通りの挙動。
for in文は処理済みのプロパティ名のみを覚えていてプロパティの数はチェックしない。未処理のプロパティが存在するか否かはループ内の処理が終わるたびにその時点でのプロパティと処理済のプロパティを比較して判断される。
と、いうことのようです。
(1/24追記。このエントリには続編があります。是非ご一読ください。)