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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章