閉包是一個比較抽象的概念,尤其是對js新手來說.書上的解釋實在是比較晦澀,對我來說也是一樣. 但是他也是js能力提升中無法繞過的一環,幾乎每次面試必問的問題,因為在回答的時候.你的答案的深度,對術語的理解以及js內部解釋器的運作方式的描述,都是可以看出你js實際水平的.即使你沒答對,也能讓考官對你的 ...
閉包是一個比較抽象的概念,尤其是對js新手來說.書上的解釋實在是比較晦澀,對我來說也是一樣.
但是他也是js能力提升中無法繞過的一環,幾乎每次面試必問的問題,因為在回答的時候.你的答案的深度,對術語的理解以及js內部解釋器的運作方式的描述,都是可以看出你js實際水平的.即使你沒答對,也能讓考官對你的水平有個評估.那麼我先來說說我對js中的閉包的理解.
閉包是很多語言都具備的特性,在js中,閉包主要涉及到js的幾個其他的特性:作用域鏈,垃圾(記憶體)回收機制,函數嵌套,等等.
在理解閉包以前.最好能先理解一下作用域鏈的含義,簡單來說,作用域鏈就是函數在定義的時候創建的,用於尋找使用到的變數的值的一個索引,而他內部的規則是,把函數自身的本地變數放在最前面,把自身的父級函數中的變數放在其次,把再高一級函數中的變數放在更後面,以此類推直至全局對象為止.當函數中需要查詢一個變數的值的時候,js解釋器會去作用域鏈去查找,從最前面的本地變數中先找,如果沒有找到對應的變數,則到下一級的鏈上找,一旦找到了變數,則不再繼續.如果找到最後也沒找到需要的變數,則解釋器返回undefined.
瞭解了作用域鏈,我們再來看看js的記憶體回收機制,一般來說,一個函數在執行開始的時候,會給其中定義的變數劃分記憶體空間保存,以備後面的語句所用,等到函數執行完畢返回了,這些變數就被認為是無用的了.對應的記憶體空間也就被回收了.下次再執行此函數的時候,所有的變數又回到最初的狀態,重新賦值使用.但是如果這個函數內部又嵌套了另一個函數,而這個函數是有可能在外部被調用到的.並且這個內部函數又使用了外部函數的某些變數的話.這種記憶體回收機制就會出現問題.如果在外部函數返回後,又直接調用了內部函數,那麼內部函數就無法讀取到他所需要的外部函數中變數的值了.所以js解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變數(包括本地變數和父級和祖先級函數的變數(自由變數))一起保存起來.也就是構建一個閉包,這些變數將不會被記憶體回收器所回收,只有當內部的函數不可能被調用以後(例如被刪除了,或者沒有了指針),才會銷毀這個閉包,而沒有任何一個閉包引用的變數才會被下一次記憶體回收啟動時所回收.
也就是說,有了閉包,嵌套的函數結構才可以運作,這也是符合我們的預期的.然後,閉包還有一些特性,卻往往讓程式員覺得很難理解.
看看下麵一段代碼.
var result=[]; function foo(){ var i= 0; for (;i<3;i=i+1){ result[i]=function(){ alert(i) } } }; foo(); result[0](); // 3 result[1](); // 3 result[2](); // 3
這段代碼中,程式員希望foo函數中的變數i被內部迴圈的函數使用,並且能分別獲得他們的索引,而實際上,只能獲得該變數最後保留的值,也就是說.閉包中所記錄的自由變數,只是對這個變數的一個引用,而非變數的值,當這個變數被改變了,閉包里獲取到的變數值,也會被改變.
解決的方法之一,是讓內部函數在迴圈創建的時候立即執行,並且捕捉當前的索引值,然後記錄在自己的一個本地變數里.然後利用返回函數的方法,重寫內部函數,讓下一次調用的時候,返回本地變數的值,改進後的代碼:
var result=[]; function foo(){ var i= 0; for (;i<3;i=i+1){ result[i]=(function(j){ return function(){ alert(j); }; })(i); } }; foo(); result[0](); // 0 result[1](); // 1 result[2](); // 2