JavaScript のクロージャでよくある間違いを見事にする

ご飯を食べに行った話を書いたときに、その場所とブログのエントリを Google Map にマッピングしたろかいなと思いまして、今更ながら Google Maps API を見て作り中。

最初、del.icio.us にブックマークしてそれ引っ張ってきて、とか思ってたんですが、WordPress のページ作成でそれ用のページを作成、そのコンテンツに {住所},{お店の名前},{ブログのエントリの ID} 的な感じで追加していって、それを WordPress の関数で引っ張ってきて GClientGeocoder で緯度経度に変換してマーカを作って貼り付ける感じにしてみます。

ご飯マップ、超作り中ですが今ソースはこんな感じ。

PHP:
  1. ?php                                                                                                                                                             
  2. /*                                                                                                                                                                 
  3. Template Name: ご飯マップ                                                                                                                                         
  4. */
  5. ?>
  6. <?php get_header(); ?>
  7. <!-- Google Map start -->
  8. <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=YOUR_GOOGLE_MAPS_API_KEY" type="text/javascript"></script>
  9. <!-- Google Map end -->
  10. <div id="content" class="widecolumn">
  11.  
  12. <div id="gmap" style="width: 600px; height: 600px;"></div>
  13.  
  14. <?php if (have_posts()) : while (have_posts()) : the_post(); ?>
  15. <?php
  16. // WordPress の DB  からコンテンツを取得
  17. $favorites = array();
  18. $tmp = explode("\r\n", get_the_content());
  19. foreach ($tmp as $s) {
  20.     $e = explode(',', $s);
  21.     // 住所をキー、1 番目の要素をお店の名前、2 番目の要素を記事へのリンクとする配列を値とするハッシュ                                                                 
  22.     $favorites[$e[0]] = array($e[1], get_permalink($e[2]));
  23. }
  24. ?>
  25. <?php endwhile; endif; ?>
  26.  
  27. <script type="text/javascript">
  28.  
  29. function init() {
  30.     if (GBrowserIsCompatible()) {
  31.         <?php
  32.         foreach ($favorites as $address => $info) {
  33. echo <<<JS
  34.             favorites['$address'] = ['$info[0]', '$info[1]'];
  35. JS;
  36.         }
  37.         ?>
  38.  
  39.         map = new GMap2(document.getElementById("gmap"));
  40.         map.addControl(new GSmallMapControl());
  41.         map.addControl(new GMapTypeControl());
  42.         // 目黒区自由が丘を中心に設定
  43.         map.setCenter(new GLatLng(35.609393, 139.668165), 12);
  44.         var geocoder = new GClientGeocoder();
  45.         for (var addr in favorites) {
  46.             // *1
  47.             geocoder.getLocations(addr, function(address) {
  48.                 // *2
  49.                 if (address.Status.code != 200) {
  50.                     // エラー
  51.                     return;
  52.                 }
  53.                 // マーカを貼り付ける
  54.                 var p = new GLatLng(address.Placemark[0].Point.coordinates[1],
  55.                                     address.Placemark[0].Point.coordinates[0]);
  56.                 var marker = new GMarker(p);
  57.                 map.addOverlay(marker);
  58.                 // マーカにクリック処理を設定する
  59.                 GEvent.addListener(marker, 'click', function() {
  60.                     marker.openInfoWindow(document.createTextNode(favorites[addr][0]));                                                                             
  61.                 });
  62.             });
  63.         }
  64.     }
  65. }
  66.  
  67. var map = null;
  68. var favorites = {};
  69. init();
  70. </script>
  71.  
  72. </div>
  73. <?php get_footer(); ?>

なんつう感じでやってみてると、for (var addr in favorites) のループの *1 で alert(addr) した結果と *2 で alert(addr) した結果が違って全部のお店の名前が favorites の最後のお店の名前に。アガ。

で、javascript、スコープ、クロージャくらいで調べてみると、mozilla にドキュメントありました。「 よくある間違い: ループ内でクロージャを作成する」。なるほど。クロージャそれぞれにスコープを分けてあげないといけないのね。

というわけでちと修正。javascript 部分を抜き出してみます。

JAVASCRIPT:
  1. <script type="text/javascript">
  2. function makeAddMakerCallback(addr, name, link) {
  3.     return function(address) {
  4.         if (address.Status.code != 200) {
  5.             // エラー
  6.             return;
  7.         }
  8.         // マーカを貼り付ける
  9.         var p = new GLatLng(address.Placemark[0].Point.coordinates[1],
  10.                             address.Placemark[0].Point.coordinates[0]);
  11.         var marker = new GMarker(p);
  12.         map.addOverlay(marker);
  13.         // マーカにクリック処理を設定する
  14.         GEvent.addListener(marker, 'click', function() {
  15.             marker.openInfoWindowHtml("<a href='" + link + "' target='_blank'>" + name + "</a>");
  16.         });
  17.     }
  18. }
  19.  
  20. function init() {
  21.     if (GBrowserIsCompatible()) {
  22.         <?php
  23.         foreach ($favorites as $address => $info) {
  24. echo <<<JS
  25.             favorites['$address'] = ['$info[0]', '$info[1]'];
  26. JS;
  27.         }
  28.         ?>
  29.  
  30.         map = new GMap2(document.getElementById("gmap"));
  31.         map.addControl(new GSmallMapControl());
  32.         map.addControl(new GMapTypeControl());
  33.         // 目黒区自由が丘を中心に設定
  34.         map.setCenter(new GLatLng(35.609393, 139.668165), 12);
  35.         var geocoder = new GClientGeocoder();
  36.         for (var addr in favorites) {
  37.             geocoder.getLocations(addr,
  38.                                   makeAddMakerCallback(addr,
  39.                                                        favorites[addr][0],
  40.                                                        favorites[addr][1]));
  41.         }
  42.     }
  43. }
  44.  
  45. var map = null;
  46. var favorites = {};
  47. init();
  48. </script>

マーカを作成するのに必要なデータを受け取ってそれを元にマーカを設定する関数自体を返す関数を作成して(なげえ)、geocoder.getLocations() に渡すように変更。これで無事個別の値を参照するようになりました。

うーん、やる気はあんまりないもののもうちっと勉強しないとダメですねこりゃ。


About this entry