1. 程式人生 > >for迴圈中的閉包問題及解決方案

for迴圈中的閉包問題及解決方案

說到閉包,我們首先來看一個最最簡單的例子,也是最最基礎的例子:為多個相同的元素,繫結事件,在點選每一個元素時,提示被點選元素的排列位置。

<span style="font-size:14px;">    <div id = "test">
        <p>欄目1</p>
        <p>欄目2</p>
        <p>欄目3</p>
        <p>欄目4</p>
    </div></span>

拿到手的第一反應就是for迴圈新增點選事件了(新增索引值也可以!)

這裡討論閉包解決!(i=4   ,一直彈4,好煩!)

<span style="font-size:14px;">function bindClick(){
        var allP = document.getElementById("test").getElementsByTagName("p"),
            i=0,
            len = allP.length;
            
        for( ;i<len;i++){
            allP[i].onclick = function(){    //匿名函式作為回撥函式
                alert("you click the "+i+" P tag!");
                //you click the 4 P tag!
            }
        }
    }
    bindClick();
    //執行函式,繫結點選事件</span>

這樣的JS處理,看起來沒有問題,可是在測試的時候,不管我們點選哪一個p標籤,我們獲取到的結果都是相同的,tell me why?說白了,這就是作用域到導致的一個問題。

下面來分析一下原因。首先呢,我們先把上述的JS程式碼給分解一下,讓我們看起來更容易理解。

<span style="font-size:14px;">    function bindClick(){
        var allP = document.getElementById("test").getElementsByTagName("p"),
            i=0,
            len = allP.length;
        
        for( ;i<len;i++){
            allP[i].onclick = AlertP;
        }
        function AlertP(){   //非匿名函式作為回撥函式
            alert("you click the "+i+" P tag!");  //發現i是未知的,沿著作用域查詢i,但是i是經過for迴圈後得到的值,i=4
        }
    }
    bindClick();
    //執行函式,繫結點選事件</span>

這裡應該沒有什麼問題吧,前面使用一個匿名函式作為click事件的回撥函式,這裡使用的一個非匿名函式,作為回撥,完全相同的效果。也可以做下測試哦。

理解上面的說法了,那麼就可以很簡單的理解,為什麼我們之前的程式碼,會得到一個相同的結果了。首先看一下for迴圈中,這裡我們只是對每一個匹配的元素添加了一個click的回撥函式,並且回撥函式都是AlertP函式。

這裡當為每一個元素新增成功click之後,i的值,就變成了匹配元素的個數,也就是i=len,而當我們觸發這個事件時,也就是當我們點選相應的元素時,我們期待的是,提示出我們點選的元素是排列在第幾個,這個時候,click事件觸發,執行回撥函式AlertP

但是當執行到這裡的時候,發現alert方法中,有一個變數是未知的,並且在AlertP的區域性作用域中,也沒有查詢到相應的變數,那麼按照作用域鏈的查詢方式,就會向父級作用域去查詢,這裡的父級作用域中,確實是有變數i的,而i的值,卻是經過for迴圈之後的值,i=len。所以也就出現了我們最初看到的效果。

瞭解了這裡的原因,那麼解決方法也就很簡單了,控制這個作用域的問題唄,說白了,也就一個方法,那就是在回撥函式中,

用一個區域性變數,來記錄這個i的值,這樣當再區域性作用域中使用到i變數時,就會使用優先使用區域性變數中的i變數的值。不會再去查詢全域性變量了。(定義索引值也是這個原理)


說到了這裡,大概也能理解一下閉包的概念了,按照之前我們說的作用域鏈的說法,當一個函式執行時,該函式就會被推入作用域鏈的前端,當函式執行結束,這個函式就會被推出作用域鏈,並且銷燬函式內部的區域性變化和方法。

PS:閉包,說白了也就是在函式執行結束,作用域鏈將函式彈出之後,函式內部的一些變數或者方法,還可以通過其他的方法引用。

但是這裡呢,當bindClick執行結束後,依然可以通過click事件訪問到bindClick函式內部的i變數,說明bindClick函式內部的i變數,在bindClick結束後,並沒有被銷燬,這也就是閉包了。

重點來了,如何解決for迴圈中的閉包問題呢?

方法1:使得繫結click事件的目標物件和變數i都變成區域性變數。這裡可以直接把這兩者作為形參,傳遞給另外的一個函式即可。(閉包中的傳參)

<span style="font-size:14px;">    function bindClick(){
        var allP = document.getElementById("test").getElementsByTagName("p"),
            i=0,
            len = allP.length;
        
        for( ;i<len;i++){
            AlertP(allP[i],i);
        }
        
        function AlertP(obj,i){
            obj.onclick = function(){
                alert("you click the "+i+" P tag!");
            }
        }
    }
    bindClick();</span>

這裡,objiAlertP函式內部,就是區域性變量了。click事件的回撥函式,雖然依舊沒有變數i的值,但是其父作用域AlertP的內部,卻是有的,所以能正常的顯示了,這裡AlertP我放在了bindClick的內部,只是因為這樣可以減少必要的全域性函式,放到全域性也不影響的。

方法2.方法1添加了一個函式進行繫結,如果我不想新增函式呢!(

自執行函式

<span style="font-size:14px;">    function bindClick(){
        var allP = document.getElementById("test").getElementsByTagName("p"),
            i=0,
            len = allP.length;
        
        for( ;i<len;i++){
            allP[i].onclick = function (i){
                return function(){
                    alert("you click the "+i+" P tag!");
                }
            }(i);
        }
    }
    bindClick();</span>

閉包的應用

OK,這也是閉包的最簡單的應用了,其他的閉包寫法也有,只是就原理方面來說,和上面這種是相同的原理,所以這裡就不一一列舉了,用到閉包的地方其實很多(比如惰性載入函式,單例模式中的物件定義等),如果您能理解到這最簡單閉包的原理,那麼其他用到閉包的地方,見到了,也就能理解了。或者說,想要使用的時候,也就能想到應該怎麼用了吧。