2012/08/26(日)JavaScriptの無名function内にバインドされる変数のスコープがC#と違う

はてブ数 2012/08/26 17:38 プログラミング::JavaScriptつーさ

ボタンをスクリプトでたくさん作って、それぞれの clickイベントの function を作るときに、
そのfunctionの外側で宣言された変数がどうbindしたらいいのか。

<div id="test"></div>

<script>
for (var i = 0; i < 10; i++) {
  var input = document.createElement('input');
  var j = i;
  input.type = 'button';
  input.value = input.id = 'button_' + i;
  input.addEventListener('click', function() { alert(j); });
  document.getElementById('test').appendChild(input);
}
</script>

やりたいことはわかってもらえると思う?
でも、どのボタンを押しても表示されるのは9。これじゃあだめ。

というか、一瞬自分の書いたスクリプトに馬鹿にされてる気がしてきた。9的な意味で。

どーやるんだろう。

そもそも、元々上のよーなスクリプトを書いたのは、
我らがおなじみC#で、次の2つのコードは出力結果が違うから。
(ラムダを使ってないのは話をわかりやすくするためってことで)

class Program
{
    delegate int Func();
    static void Main()
    {
        List<Func> list = new List<Func>();
        for (int i = 0; i < 5; i++)
        {
            Func f = delegate() { return i; };
            list.Add(f);
        }

        foreach (Func f in list)
            Console.WriteLine(f());
    }
}

// Outputs:
// 5
// 5
// 5
// 5
// 5

class Program
{
    delegate int Func();
    static void Main()
    {
        List<Func> list = new List<Func>();
        for (int i = 0; i < 5; i++)
        {
            int j = i; // 一端jに代入している。
            Func f = delegate() { return j; };
            list.Add(f);
        }

        foreach (Func f in list)
            Console.WriteLine(f());
    }
}

// Outputs:
// 0
// 1
// 2
// 3
// 4

当然、意図するのは後者の出力結果だ。

変数jは、ループ内で宣言されるから、それはつまり、ループ回数分宣言されるわけで、
各周回のdelegateの中身は全部別の変数を参照する、と理解してる。

delegateというか、ラムダというか無名関数みたいなものを僕はC#で覚えたので、
JavaScriptでも同じようなもんだろうと思って書いたのが冒頭のスクリプト。再掲。

<div id="test"></div>

<script>
for (var i = 0; i < 10; i++) {
  var input = document.createElement('input');
  var j = i;
  input.type = 'button';
  input.value = input.id = 'button_' + i;
  input.addEventListener('click', function() { alert(j); });
  document.getElementById('test').appendChild(input);
}
</script>

JavaScriptには、同じ考え方が通用しなかった!
どのボタンを押しても全部 9 。
変数 j はこの世に1つしかないらしい。

えーっとどうやればいいんだろう?

↓こういう感じにしないといけないらしい。

for (var i = 0; i < 10; i++) {
  var input = document.createElement('input');
  input.type = 'button';
  input.value = input.id = 'button_' + i;
  input.addEventListener('click', function(j) { return function() { alert(j); } }(i));
  document.getElementById('test').appendChild(input);
}

functionの仮引数(という呼び方がJavaScriptにおいて正しいかどうか知らないが)は、
ちゃんと別の世界の変数として用意されるみたいだ。むむむ。めんどくさ。

ちょっとHTML触ってて嵌まり、今後気をつけないといけないポイントだと思ったのでなんとなく記事化。
そういえば、どっちもこの辺の言語仕様についてちゃんとドキュメントを読んだ記憶がないなぁ……。
なんかこういうもんなんだって感じで覚えちゃってる。あんまよくないね。
殊JavaScriptに至っては実装がいくつもあるし……。

あ、変数をバインドしないで

input.addEventListener('click', function() { alert(this.id); } );

とかやる方法もないこともない、というかその方がいいのかもしれ。