JavaScriptのfor文内でAjaxを使った時に、少し予想と異なる挙動となったのでそのメモ。
まずは下記のように、順序リストをHTML中に指定。
<ol> <li></li> <li></li> <li></li> <li></li> </ol>
その後、1.txt、2.txt、3.txtファイルの中の記述をそれぞれのファイル名の数値に対応するliタグの中に入れるという処理を行うことに(それぞれのテキストファイルは、『1です。』『2です。』『3です。』と書いてあります)。
というわけで、書いてみたのが以下のようなコード。
$(function(){ for(var i=1;i<=3;i++){ $.ajax({ type:'GET', url:i + '.txt', success:function(data){ $('ol li:nth-child(' + i + ')').append(data); } }); } });
for文でカウンタ変数iを1~3までの整数をとるようにし、AjaxのURLを『i + '.txt'』(つまり、iが1なら"1.txt")とし、Ajaxが成功した場合はolタグ内のi番目のliタグ内に取得したデータをいれるという流れとなっています。
実行サンプル:ループの中でのAjax
予想では、それぞれのファイルの中身はファイル名に対応するliタグに入るはずが、すべて4番目のliタグ内に入ってしまっています。
これは考えてみたら当たり前のことで、Ajaxが成功した時に実行する関数は、for文を抜けた状態、つまりiが4となっているため4番目のliタグ内にAjaxで受け取ったデータが入ったわけです。このあたりは、JavaScriptのクロージャをちゃんと理解できていたら分かるとは思うのですが、ちょっと複雑なので自分も深くまでは理解できていません(参考:クロージャ - JavaScript | MDN)。
というわけで、先ほどのループ中でのAjaxの解決方法について。上記の場合、ループ用の変数のiと、Ajaxの成功時の関数の中のiが同じだったので起こった問題です。ということは、クロージャを一つ追加してループ用のiとAjaxを呼び出す時のiを別々のものと考えたいいのではないかと思って書いてみたのが以下。
$(function(){ for(var i=1;i<=3;i++){ (function(i){ $.ajax({ type:'GET', url:i + '.txt', success:function(data){ $('ol li:nth-child(' + i + ')').append(data); } }); })(i); } });
ajaxの実行箇所を引数にiをつけた無名関数内にいれるようにしました。これで、ループ用のiとAjax内でのi(無名関数中でのi)は別々のものという扱いになりました。
実行サンプル:ループの中でのAjax
うまくいったようです。
ちなみに、どちらも変数名をiとしてしまったので、ちょっと分かりにくかったかもしれないので少し変更すると、上記は下記のように書いたのと同じことです(無名関数内のiをjに変更)。
$(function(){ for(var i=1;i<=3;i++){ (function(j){ $.ajax({ type:'GET', url:j + '.txt', success:function(data){ $('ol li:nth-child(' + j + ')').append(data); } }); })(i); } });
クロージャはうっかりするとハマりそうですね(もちろん、悪い状況という意味での『ハマる』)。この機能はメモリリークが起きるんじゃないかと思わせますし、実際調べてみるとそうらしいのですが、慣れれば便利そうです。
2015年9月5日追記:
コメントでいただいたとおり、今回の実装は順番の保証はしていません。
1. 2です。
2. 1です。
3. 3です。
と可能性もあります。
コメント
これだと、
success:function(data)
の中身が複雑な処理だった場合、
2です。
1です。
3です。
のように、順番が変わっていくと思います。
(処理が終わった順になると思うので)
コメントありがとうございます。
ファイルサイズが、2>1>3とかでも、そういうことになりそうですね。
今回の記事は、あくまで、カウンタ変数をAjax成功時の関数の内部で使いたいというだけで、順番は保証しないという注意を入れておくことにします。