いつか絶対に引っかかるというJavaScriptの予言が的中した


「ん? 変数の値が上書きされてる!」

「あれー。なんかループの中のデータがおかしい!」とか、「ん?変数の値が上書きされてる!」とかいう事態が起きたら、この投稿を思い出してください。

予言しよう!あなたはいつか、この罠を踏む。
いつかあなたが絶対に引っかかる、ある一つのJavaScriptの罠 - Qiita

ついに引っかかってしまった。

class Trap
  constructor: ->
    @prefix = '#btn-'
    @types = ['hoge', 'hage', 'fuge']
    @bind @types

  bind: (types) ->
    for type in types
      $(@prefix + type).click =>
        @setType(type)

  setType: (type) ->
    console.log "Set #{type}."

これを実行すると、どのボタンをクリックしても結果がSet fuge.になってしまう。

よくある失敗の例だ。ループの中で関数を定義しているわけだが、ループ変数は関数の外部で定義されている。クリックしたときに実行する関数は、実行時に i を参照するが、それはループが終わったときの値だ。従って、今回の例の場合は3が表示される。
JavaScript のクロージャとループ、変数について | UB Lab.

回避方法

ループ内で関数を定義していて、ループ変数の値を表示するには、関数を戻す関数を定義する必要がある。下記の例では、クリックしたときに実行される関数は、関数を戻しており、かつ、関数自体を呼び出すように指定して引数にループ変数を渡している。console.log で渡している引数は、最初の関数の引数と同じものを参照するので、ループしている間の i の値を表示することができる。

for(var i = 0; i < array.length; i++) {
    array[i].click((function(n) {
        return function() {
            console.log(n);
        }
    }).call(this, i));
}

JavaScript のクロージャとループ、変数について | UB Lab.

これCoffeeScriptでどうやって書くの?

2015-08-24 更新

bind: (types) ->
  for type in types
    do (type) =>
      $(@prefix + type).click => 
        @setType(type)

@pomutemuさんがコメントで綺麗な書き方を教えてくださったので、内容を更新しました! codepenライブデモ


以下旧版
デザイナー崩れの脳味噌では上手く書けなかったので便利なツールに頼ることにした。

bind: (types) ->
  for type in types
    $(@prefix + type).click ((t) =>
      =>
        @setType t
    ).call @, type

これで無事、ループ内の変数をコールバックの引数に渡すことができた。