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