1547


要素`#elem0` .. `#elem9`に対してクリックイベントを設定し、それぞれの要素に対応した番号を`alert`で出したいと思っています。

下記のコードだと、`#elem0`をクリックしたときには`0`、`#elem1`をクリックしたら`1`、といったようにです。しかし、下のコードだと常に10が表示され、上記のような挙動になりません。どうすればいいのでしょうか?

[code]

    for (var i = 0; i < 10; i++) {
        $('#elem' + i).click(function() {
            alert(i);
        });
    }
    
[/code]


CC BY-SA 3.0
2014-12-12T06:39:50.897
1534
2019-12-13T13:54:21.457
2014-12-12T14:25:51.250
939
525
question
javascript jquery
for文でclick時の動作を定義したい


この場合は下記のように関数を返す関数に引数として渡す事で望む動作が可能です。

[code]

    for (var i = 0; i < 10; i++) {
        $('#elem' + i).click((function(i) {
            return function() {
                alert(i);
            }
        })(i));
    }
    
[/code]

理屈ではわかっても、普通の書き方に慣れているとつい油断してしまいます。


CC BY-SA 3.0
2014-12-12T06:39:50.897
1535
2014-12-12T06:39:50.897
525
1534
answer


引数を使わず以下の様にbindする事もできます。

[code]

    for (var i = 0; i < 10; i++) {
        $('#elem' + i).click((function() {
            var x=i;
            return function() {
                alert(x);
            }
        })());
    }
    
[/code]


CC BY-SA 3.0
2014-12-12T07:30:09.167
1536
2014-12-12T07:30:09.167
728
1534
answer


flied onionさんも、Taizan Yonetujiさんも正解を書かれていると思います。  
ここでは後から回答を読まれる方のために少し説明を書きます。

**問題が発生する理由**

問題のコードでは、どのエレメントをクリックしても、alert文が出力するのは"10"です。  
これはJavaScriptのクロージャの仕様によるものです。

単純な例を挙げます：

[code]

    var msg = "foo";
    $('#btn').click(function() {
        alert(msg); // => "bar"
    });
    msg = "bar";
    
[/code]

DEMO :

[code]

    var s = "foo";
    $('#btn').click(function() {
         alert(s);
    });
    s = "bar";
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <input type="button" id="btn" value="click me">
[/code]

この例でalert文が出力するのは、"foo"ではなく"bar"になります。  
`click()`のハンドラ関数がバインドされて実行された時には、変数`msg`の値が"bar"になっているからです。

**即時関数を使った解決法**

解決法のひとつは即時関数を使うものです。即時関数はその名の通り即時に実行されるので、即時関数の中で`var`を使って新規に変数を定義すると、その変数の値は即時関数が定義された時（＝実行された時）のものになります。

[code]

    var msg = "foo";
    (function(){
      var s = msg;
      $('#btn').click(function() {
         alert(s);　// => "foo"
      });
    })();
    msg = "bar";
    
[/code]

DEMO :

[code]

    var msg = "foo";
    
    (function(){
      var s = msg;
      $('#btn').click(function() {
         alert(s);
      });
      
    })();
    msg = "bar";
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <input type="button" id="btn" value="click me">
[/code]

即時関数には引数を渡すこともできるので、同じ内容を以下のように書くこともできます。

[code]

    var msg = "foo";
    (function(s){
        $('#btn').click(function() {
            alert(s); //=> "foo"
        });
    })(msg);
    msg = "bar";
    
[/code]

DEMO :

[code]

    var msg = "foo";
    
    (function(s){
      
      $('#btn').click(function() {
         alert(s);
      });
      
    })(msg);
    msg = "bar";
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <input type="button" id="btn" value="click me">
[/code]

* * *

**forループの問題への回答例**

前置きの説明が長くなりましたが、forループの問題は以下のように書くことで解決できます。

[code]

    for (var i = 0; i < 10; i++) {
        (function (n) {
            $("#elem" + i).click(function(){
                alert(n);
            });
        })(i);
    }
    
[/code]

DEMO :

[code]

    for (var i = 0; i < 10; i++) {
        (function (n) {
            $("#elem" + i).click(function(){
                alert(n);
            });
        })(i);
    }
[/code]

[code]

    div {
        width:100px;
        border:1px gray solid;
        padding:0.5em;
    }
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="elem0">0</div>
    <div id="elem1">1</div>
    <div id="elem2">2</div>
    <div id="elem3">3</div>
    <div id="elem4">4</div>
    <div id="elem5">5</div>
    <div id="elem6">6</div>
    <div id="elem7">7</div>
    <div id="elem8">8</div>
    <div id="elem9">9</div>
[/code]

**即時関数を使わないやりかた**

また、即時関数を使うことが難しい局面では、以下のようにｊQueryの`data()`関数が役に立つ場合もあります。

[code]

    for (var i = 1; i < 10; i++) {
    　　　　$("#elm" + i).data("n",i).click(function () {
        　　　　alert($(this).data('n'))
    　　　　});
    }
    
[/code]

DEMO :

[code]

    for (var i = 0; i < 10; i++) {
      $("#elm" + i).data("n", i).click(function() {
        alert($(this).data('n'));
      });
    }
[/code]

[code]

    div {
      width: 100px;
      border: 1px gray solid;
      padding: 0.5em;
    }
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="elm0">0</div>
    <div id="elm1">1</div>
    <div id="elm2">2</div>
    <div id="elm3">3</div>
    <div id="elm4">4</div>
    <div id="elm5">5</div>
    <div id="elm5">6</div>
    <div id="elm7">7</div>
    <div id="elm8">8</div>
    <div id="elm">9</div>
[/code]

以上です。


CC BY-SA 4.0
2014-12-12T10:20:43.147
1547
2019-12-13T13:54:21.457
2019-12-13T13:54:21.457
32986
47
1534
answer


解答は既に他の人が書いているので、質問の挙動からして補足説明しますと、これは **変数のスコープ** を勘違いしているために起こるものだと思われます。

JavaScriptの仕様だと、通常関数内で定義されていない変数を利用しようとしたさい、それよりも前の変数を参照しようとします。例として、次のようなコードが書けます。

[code]

    var i = 0;
    var a = function () {
        console.log("var i:" + i);
        var b = function () {
            var i = 99;
            var c = function () {
                console.log("var i:" + i);
            };
        c();
        }
        b();
     }
    
[/code]

このとき、なんらかのブラウザのJavaScriptコンソールで、`a();`を実行すると、最初に`var i: 0`が出力され、次に`var i:
99`が出力されるかと思われます。この理由としては、`c()`の関数内で一番近い`var
i`の宣言が`b();`の中で行われているもので、`a()`の関数は、その外の`var i`が一番近いものだからです。

質問に戻りますと、この場合の`click`内で使われている`i`は、`for`で使われている値です。そして、forで使われたiは実行が終了したときに、値が`10`にセットされているわけです。そうすると、`click()`したときに参照される値は`10`にセットされた`i`を見にいくわけですから、全部の出力がiとなるわけです。

そのため、この変数の状態を、なんらかの形で保存しておく必要があるわけですが、このときに利用される手法が **クロージャ**
と呼ばれるものです。この実例は他の方が書いている通りなので、この解説では省略します。


CC BY-SA 3.0
2014-12-12T14:17:49.337
1564
2014-12-12T14:17:49.337
939
1534
answer


click や setTimeout で実行される関数は実行時に評価されます。ですので値を束縛してやる必要があります。

[code]

    for (var i = 0; i < 10; i++) {
        (function(i) {
            $('#elem' + i).click(function() {
                alert(i);
            });
        }(i);
    }
    
[/code]

この方法は即時関数を for ループ毎に定義する事で引数を用いて値を束縛する方法です。 詳しくはこちらをご覧ください。

<http://mattn.kaoriya.net/software/lang/javascript/20110523124612.htm>


CC BY-SA 3.0
2014-12-16T01:31:37.767
1616
2014-12-16T01:31:37.767
440
1534
answer


bind関数は、bind関数を **呼出した時** の値をバインドします。 次のように書けます。

[code]

    for (var i = 0; i < 10; i++) {
        $('#elem' + i).click(
            alert.bind(null, i)
        );
    }
    
[/code]

bind関数はECMAScript 5で追加されました。 IE9以降のモダンブラウザで使えます。 詳しくは
[Function.prototype.bind() - JavaScript |
MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)をみてください。


CC BY-SA 3.0
2014-12-16T01:41:51.160
1620
2014-12-16T01:41:51.160
2325
1534
answer


[JSHint](http://jshint.com/)を使うとこの種のポカミスを防げるのでおすすめです。 この質問のコードの場合、`Don't make
functions within a loop.` ([解説](https://jslinterrors.com/dont-make-functions-
within-a-loop)) という警告を出してくれます。


CC BY-SA 3.0
2014-12-16T17:53:03.023
1809
2014-12-16T17:53:03.023
4137
1534
answer


ES2015であれば、ブロックスコープである const や let を使うことでも回避できます。例えば下の例では各ループの `x`
は別の変数になりますから、期待通り動作するかと思います。

[code]

    for (var i = 0; i < 10; i++) {
        let x = i;
        $('#elem' + x).click(function() {
            alert(x);
        });
    }
    
[/code]

さらに、ループカウンタ変数の宣言に let を使うことでも同じような効果が得られます。

[code]

    for (let i = 0; i < 10; i++) {
        $('#elem' + i).click(function() {
            alert(i);
        });
    }
    
[/code]

ただし、他の回答のように `click()` を呼び出した時点で値を束縛しているわけではありませんから、`click()`
の後に値を変更した場合はそれが反映されることになります。

[code]

    for (var i = 0; i < 10; i++) {
        let x = i;
        $('#elem' + x).click(function() {
            alert(x); // 常に 12345 が表示される
        });
        x = 12345;
    }
    
[/code]

参考

  * [JavaScriptのクロージャ入門とECMAScript 6のletキーワードによる変数宣言 « LINE Engineers' Blog](http://developers.linecorp.com/blog/ja/?p=3428)


CC BY-SA 3.0
2016-06-21T07:08:24.783
26912
2016-06-21T07:08:24.783
8000
1534
answer