JavaScript Closure

這篇想寫很久了,test case的檔案也寫好超過一個月了,這次用了3(?)個case來說明,以下每個test-case都有三個h2標籤,然後透過getElementsByTagName來取得這三個h2,接著用for迴圈來對每個h2加上click事件,click後會alert i的值出來,i是for迴圈的索引值。首先來看一下第一個case的script:

var subHeads = document.getElementsByTagName('h2');
for (var i=0; i<subHeads.length; i++) {
    subHeads[i].onclick = function () {
        alert(i);
    };
}

這個case中,使用非常直觀的想法,不過他的結果是每個h2 click下去都會alert "3"出來,實際上每個click都是執行一個function,這個function會執行alert function,並且送 i 這個變數作為輸入值,不過實際上使用者click動作發生時,迴圈已經跑過一遍,而且 i 的值已經變成3了,所以你不管click哪一個h2,實際上都是做 alert(3) 這個動作。

要解決這個問題,有兩個方法可以處理,第一個是利用function來做closure把變數的scope獨立出來,另一個方法是用eval來做function,首先來看第一個作法

var subHeads = document.getElementsByTagName('h2');
for (var i=0; i<subHeads.length; i++) {
    (function () {
        var ii = i;
        subHeads[i].onclick = function () {
            alert(ii);
        };
    })();
}

這個方法是用匿名function把 ii 這個變數的scope獨立起來,而 ii 的值就是在這個匿名function執行時 i 的值,這樣每個click function裡面的 ii 就都各自獨立,不會互相影響到。再來看第二個作法

var subHeads = document.getElementsByTagName('h2');
for (var i=0; i<subHeads.length; i++) {
    eval("subHeads[i].onclick = function () {" +
         "    alert("+i+");" +
         "};");
}

可以看到整個event function的指派都是用eval來達成的,比較特別的是要用 i 時,我是跳脫字串,直接用 i變數 的值,這個作法其實是讓每個click function的內容都不太一樣(alert的輸入值不同)。而除了這兩個方法之外,我還蠻喜歡把屬性加到DOM的elementNode上的,所以來看看我習慣的作法

var subHeads = document.getElementsByTagName('h2');
for (var i=0; i<subHeads.length; i++) {
    subHeads[i].attr_i = i;
    subHeads[i].onclick = function () {
        alert(this.attr_i);
    };
}

在這個範例理,我先在 h2 node 下面加上 attr_i 這個屬性,值就是 i 當時的值,而click function內就是送 attr_i 給alert function,這樣結果也可以如我們所預期。不過如果是用jQuery的話就要注意了,來看看最後這個jQuery例子

var subHeads = $('h2');
for (var i=0; i<subHeads.length; i++) {
    subHeads.eq(i).attr_i = i;
    subHeads.eq(i).click(function () {
        alert($(this).attr_i);
    });
}

因為jQuery沒有cache機制,所以上面這個例子會發生錯誤, $(this).attr_i 會不存在,會alert出 'undefined' ,要新增屬性的話要直接加到最基本的 DOM Node 上,不可以放在jQuery物件上,要存取DOM Node可以用subHead.get(0)或是subHead[0],後者我沒測試過就是。