http://reval-mocchi.rhcloud.com

JavaScriptと変数のスコープとクロージャと

例題

0から9のボタンがあり,クリックするとボタンに表示されている数字のalertがでるようにしたい.

よくある間違いコード

function test(){
    for(var i = 0; i < 10; i++){
        var elem = document.createElement('button');
        elem.appendChild(document.createTextNode(i));
        elem.onclick = function(){alert(i);};
        document.body.appendChild(elem);
    }
}

option:enable title:countup.js

結果として,どのボタンを押してもalertでは10が表示される.

なぜなら,JavaScriptの変数のスコープがfunctionスコープである事と,クロージャにより関数実行時のローカル変数の値を保存できてしまうからだ.

JavaScriptの変数のスコープはブロック単位ではない

JavaScriptの変数のスコープがfunctionスコープである事について説明する.

変数の巻き上げコード

function ref(){
    console.log(makimaki);
    for(var makimaki = 0;makimaki < 10;makimaki++){}
}

実行結果

> ref()
undefined
> console.log(noneVar)
ReferenceError: noneVar is not defined

このように,関数内で宣言されている場合は値がundefinedになるが,宣言されていない場合はReferenceErrorとなる.

クロージャ

クロージャにより関数の実行時のローカル変数の値を保存できてしまうからだ について説明する.

カウントアップする関数の生成機

function countupGenerator(){
    var num = 0;
    return function(){ return num++; };
}

実行結果

> var a = countupGenerator()
> a()
0
> a()
1
> var b = countupGenerator()
> b()
0
> a()
2

これによりわかることは,関数countupGenerator実行時のローカル変数numの値が保存されていた事,実行時のローカル変数numへの参照を持つ事だ.

これらによって

elem.onclick = function(){alert(i);};iは関数testが実行された時のローカル変数iへの参照を持つことになるので,10が表示される.

解決方法

function test(){
    for(var i = 0; i < 10; i++){
        var elem = document.createElement('button');
        elem.appendChild(document.createTextNode(i));
        (function(num){
            elem.onclick = function(){alert(num);};
        })(i);
        document.body.appendChild(elem);
    }
}

option:enable title:countup.js

JavaScriptはfunctionスコープであること,クロージャが値を保存できる事を利用した. 即時関数の引数として,iを渡している. JavaScriptの数値は値渡しなので,即時関数実行時iの値がnumとなる.