2013年5月27日月曜日

下部に追記有り: 2013/08/30

phiさん( @phi_jp )のブログ(TM Life)の以下の投稿を読んで。

[JavaScript]for 内でイベントリスナとか登録するときにやっちゃいがちな間違い. とその対処法を3つほど.

つい先日, というか今日, @webryone さんからのコードレビュー依頼を受け @awebprogrammer さんとレビューしてたときに見つかったバグが あまりにもあるあるw だったのでエントリーとして書かせて頂きました.(許可済)
そのバグというのは, for 内でイベントリスナを登録する際に クロージャが効いていない変数を使ってしまうというものです.

もう1つ対処法を思いついたので書いてみた。

コード(BBEdit)

// ページ内のボタンを全て取得するのを避けるためにtagNameではなくclassNameに変更
var buttons = document.getElementsByClassName('test'),
    button,
    i,
    max,
    text;
for (i = 0, max = buttons.length; i < max; i += 1) {
    button = buttons[i];
    button.onclick = function() {
      // この例では必要ないけど、このブロック内で
        // 再利用することがある場合のために変数に格納
        text = this.innerHTML;
        alert(text);
    };
}

HTMLのソース(BBEdit)

<button class="test">
    Button 1
</button>
<button class="test">
    Button 2
</button>
<button class="test">
    Button 3
</button>

ちゃんとそれぞれButton 1、Button 2、Button 3が表示される。(上記のJavaScriptのコードを評価後(eval))

コードを読めば分かる通り、それぞれのボタンのテキストを取得するのに、thisを使ってそのbutton自身のタグ内ののテキスト(innerHTML)を取得。

phiさんの3つの対処方法と比べて、柔軟性、応用性はないかもしれないけど、とりあえずサンプルのような場合で上手く動くようにすることに限っては、3つの方法よりは単純かなぁと。

おまけ

これは, for 内で定義する関数がクロージャされていない為に起きる現象です.

自分なりの分かりやすい説明を考えてみた。

  1. まずforループの終了時のbutton.innerHTMLの値は、buttons[2]のinnerHTMLの値、つまりButton 3となる。
  2. Button 1をクリックすると、

    JavaScriptのコード(BBEdit)

    function() {
        alert(button.innerHTML);  
    };
    
    が呼び出される。
  3. そして、この時にalert(button.innerHTML)が実行される。
  4. この実行される時点でのbutton.innerHTMLの値はforループ終了時のbutton.innerHTMLの値、つまり最初に述べたButton 3となる。
  5. よってButton 1をクリックすると、Button 3がalertダイアログに表示される。

こんな感じの説明で合ってるかなぁ〜

追記: 2013/08/30

イベントハンドラの this と event.target, +α - hogehoge @teramako

3.1. let 宣言によるブロックスコープ

これをfor文のブロック内で定義する変数が、ブロックスコープとして保持されない為に起きる現象と捉えてみましょう。
ということで、ECMAScript 6th から使用出来る let 宣言の登場です。

let宣言使えるようになれば、ブロックスコープに慣れてる人は快適になるかなぁと思ったけど、

ということで、letはChromeでは(他のブラウザでも(?))まだ使えないみたいなので、関数スコープをブロックスコープっぽく使ってみる方法。(関数を定義してすぐ呼び出す。)

コード(BBEdit)

// ページ内のボタンを全て取得するのを避けるためにtagNameではなくclassNameに変更
var buttons = document.getElementsByClassName('test1'),
    i,
    max;
for (i = 0, max = buttons.length; i < max; i += 1) {
    (function () {
        var button = buttons[i];
        button.onclick = function () {
            alert(button.innerHTML);
        };
    })();
}

HTMLのソース(BBEdit)

<button class="test1">
    Button 1
</button>
<button class="test1">
    Button 2
</button>
<button class="test1">
    Button 3
</button>

ちゃんとそれぞれButton 1、Button 2、Button 3が表示される。(上記のJavaScriptのコードを評価後(eval))

0 コメント:

コメントを投稿