[JS] クロージャー怖い

2014年3月20日

 先日(といってもすでに半年前…)、HiroshimaRB にて「思ったようにコードが動かない!」という題目で LT をしたのですが、JavaScript のクロージャーのところで質問がありました。私にもっと理解力があって、冷静に対応できればよかったのですが、ぐでぐでな対応になってしまったので、反省を込めてここに記す(汗

  1. 問題のコード

     サンプルコードはこのようなものでした。

    <html><body>
    
    <span id="value" style="font-size: 48pt;" ></span>
    
    <script type="text/javascript">
    
      for (i=0; i<5; i++) {
        f = function() {
          // i の値を追記する
          document.getElementById("value").innerHTML += i + "<br>";
        }
    
        // i 秒後に i を追加表示する(予定)
        setTimeout("f()", i*1000);
      }
    
    </script>
    
    </body></html>
    

     期待する結果は、1 秒ごとに 0 → 1 → 2 → 3 → 4 と表示される予定です。

    0
    1
    2
    3
    4

     しかし実行結果は、 0 → 1 → 2 → 3 → 4、とはならず、すべて 5 で表示されます。

    5
    5
    5
    5
    5

  2. 指摘の内容

     この時、for のところに var を入れればよいのでは、という指摘がありました。
     指摘の理由は、上記のコードの変数 i スコープがグローバルです。だから値を共有してしまったのではないか、ということでした。
     つまり、上記のコードの以下の部分を、

    for (i=0; i<5; i++) {
    

    以下のように「var」を追加し、変数 i のスコープをグローバルからローカルに変えれぱ解決するのではないか?という指摘です。

    for (var i=0; i<5; i++) {
    

     しかし、これでは結果は変わらず、期待した動作にはなりませんでした。

  3. グローバル変数とローカル変数

     ところで、JavaScript では、なにも宣言せずに使用を始めた変数というのはグローバル変数相当(Window(=this) のメンバー) になります。(*1)
     試してみます。

    <html>
      <body>
        <form>
          <script>
            function click1()
            {
                x = "aaa";
                alert(x);
            }
    
            function click2()
            {
                alert(x);
                alert(this.x);
                alert(window.x);
            }
          </script>
    
          <input type="button" value="ボタン1" onClick="click1()">
          <input type="button" value="ボタン2" onClick="click2()">
    
        </form>
      </body>
    </html>
    

     ボタン1 を押すと、"aaa" で alert が表示されます。
     その後、ボタン2 を押すと、click2() では x は宣言や代入をしていないはずなのに "aaa" で alert が表示されます。
     つまり click1() で代入した x のスコープはローカル変数ではなく、グローバル変数に相当することがわかります。

     x をローカル変数にするためには、代入文の前に var をつけます。(*2)
     こうすると、click1() を実行した後に click2() を実行しても、click2() の x は定義されていないため、alert は表示されません。

    <html>
      <body>
        <form>
          <script>
            function click1()
            {
                var x = "aaa";
                alert(x);
            }
    
            function click2()
            {
                alert(x);
                alert(this.x);
                alert(window.x);
            }
          </script>
    
          <input type="button" value="ボタン1" onClick="click1()">
          <input type="button" value="ボタン2" onClick="click2()">
    
        </form>
      </body>
    </html>
    
  4. タイマー要素を排して、もう一度眺めてみる

     指摘の内容は、

    • グローバル変数なので、
    • 各 function がグローバル変数を参照(共有)してしまい、

    同じ値(x の値が for を抜けた時の値)になってしまうのではないか?という話でした。この指摘は、半分あたりだとおもいます。

     しかし単純にローカル変数にしてもうまくいかないことを試してみます。
     上記コードではタイマー(SetTimeout)に渡すようになっていますが、事を単純にするため、タイマーは使わないコードに変更します。

    <html>
      <body>
        <form>
          <script>
            function click1()
            {
                var f = new Array(5);
    
                // 仕込
                for (var i=0; i<5; i++) {
                    f[i] = function() {
                        alert(i);
                    }
                }
    
                // 実行
                f[0]();
                f[1]();
                f[2]();
                f[3]();
                f[4]();
            }
          </script>
    
          <input type="button" value="ボタン1" onClick="click1()">
    
        </form>
      </body>
    </html>
    

     結果は、5 回とも "5" で alert が表示されます。

  5. 原因

     上記コードの「仕込み」の部分の「alert(i)」の部分に注目します。
     この alert(i) は for 文のループ中では実行されません。function の宣言(定義)をしているだけなんですね。
     実行はその下の「実行」のところの f[0]()~f[4]() で行っています。

     alert(i) の i の値がどこで確定(*3)するかというと、function f[i]() の宣言時ではなく、f[0](); の実行時です。
     実行は for を抜けた後です。forを 抜けた後ということは、当然 i = 5 ですから、alert もすべて 5 で表示されます。

  6. 対策

     f[i] の中の i は、値ではなく変数 i を参照しています。これが原因で、f[i] は実行したときの変数 i の値を表示してしてしまいます。
     ということは、変数 i を参照するのではなく、for を実行したときの変数 i の"値"を何かしらの方法で保存してやればよいことになります。

     手順は以下の通り。
      1.f[i]() を作る処理を g() で囲みます。
      2.g() の中で、ループ変数 i を g() のローカル変数 x に代入します。
      3.for ループの中で、g() を実行します。

     こうすると、function を使わず直截変数 i を参照しているケースと違って、以下のように動作します。

    1. g() は for ループ内で実行するので、var x = i により、変数 x に変数 i の値が代入されます。
    2. g() は終了するとき、x は i の値で固定されます。
    3. for の 2 ループ目に g(); を実行するときは、変数 x のインスタンスは 1 ループ目のものとは違うものが与えられます。
      #変数 x は g() のスコープにいるので、前回ループで実行した g() での値は引き継がない。
    4. したがって、f[i] で参照している x は ループ中の i と同じ値になります。(*4)

     つまり、変数 i のスコープは click2() ですが、変数 x のスコープは g() です。したがって変数 x は f[i] では共有されず、それぞれ別々の値(=インスタンス)を保持することになります。

    <html>
      <body>
        <form>
          <script>
            function click2()
            {
                var f = new Array(5);
    
                for (var i=0; i<5; i++)
                {
                    g = function()
                        {
                            /* click2()のスコープである i を
                               g() の スコープである x に代入する */
                            var x = i;
    
                            f[i] = function() {
                                alert(x);
                            }
                        }
    
                    g();  // ここで g() の中の x が確定する。 
                }
    
                f[0]();
                f[1]();
                f[2]();
                f[3]();
                f[4]();
            }
          </script>
    
          <input type="button" value="ボタン1" onClick="click2()">
    
        </form>
      </body>
    </html>
    

    (*1)
     Perl もそうみたいです。(ただ、perl は var ではなく my と書くようです)
     VB(VBS や VBA も)は、ルーチン内で代入したものはデフォルトではローカルスコープ。但し、あとから同名のグローバル変数を作るとグローバル化してしまう。
     JavaScript のスコープは結構変態です。(と思う)

    (*2)
     実は var は後に記述してもいいらしいです。
     宣言を後に回した記述例。こう書いても x はローカル変数として認識します。
     この点も変態ですね。

    function click1()
    {
        x = "aaa";
        alert(x);
    
        var x
    }
    

    (*3)
     正確には i の値を「確定」しているわけではなく、ただ単純に i の値が参照されているだけですが…

    (*4)
     function が return すればローカル変数も解放されるはずですが、この場合はされません。
     ローカル変数のキャプチャという機能によって、そのローカル変数を参照している function がいなくなるまで保持されています。






タグ:
カテゴリー: Java Script, Program

Follow comments via the RSS Feed | Leave a comment | Trackback URL

コメントを投稿する

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


«   »
 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org