對js的閉包看了很多遍,只是每次都沒有進行深入的瞭解,過去了就忘了。
這次也算比較深入的瞭解了一下吧,不過應該也有很多不足的地方,嘿嘿!
在我們開始探索閉包前,我們首先應該理解清楚一些必要的概念,這樣對我們之後理解閉包有很大的幫助。
每個變數都有其作用的範圍,全域性變數全域性有效,會一直駐紮在記憶體中;區域性變數一般情況下會在函式執行完畢之後,就會銷燬。這裡要提到一個作用域鏈的重要概念,作用域鏈是當程式碼在一個環境中執行時建立的,作用域鏈的用途就是要保證執行環境中能有效有序的訪問所有變數和函式。作用域鏈的最前端始終都是當前執行的程式碼所在環境的變數物件,下一個變數物件是來自其父親環境,再下一個變數物件是其父親的父親環境,直到全域性執行環境。其實,通俗的理解就是:在本作用域內找不到變數或者函式,則在其父親的作用域內尋找,再找不到則到父親的父親作用域內尋找,直到在全域性的作用域內尋找!
一般情況下呢,當函式執行完畢後,區域性變數就會被銷燬,記憶體中僅儲存這全域性作用域。這裡也涉及到了js的垃圾回收機制,在js中有兩種垃圾收集的方式:標記清除和引用計數。標記清除:垃圾收集器在執行時會給儲存在記憶體中的所有變數都加上標記(具體的標記方式暫時就不清楚了),待變數已不被使用或者引用,去掉該標記或新增另一種標記。最後,垃圾收集器完成記憶體清除工作,銷燬那些已無法訪問到的這些變數並回收他們所佔用的空間。
不過,閉包可不是說函式執行完了,變數就回收了。
這裡簡單解釋一下閉包的概念:閉包使之有權訪問另一個函式作用域中的變數的函式。建立閉包的常見方式,就是在一個函式的內部建立另一個函式。
首先我們看一個簡單的例子。
function createA(){ var c = 0; return function(){ c++; return c; } }
這就是一個簡單的閉包了: var func = createA(); 從程式碼中可以看出,func是一個Function型別的變數,讓我們執行以下:func();結果會發現輸出了1。多次執行以下func()函式,發現輸出的結果一直在增長。這就說明,在func()執行之後,變數c依然儲存在記憶體中,沒有被釋放掉。在return的function中能夠使用變數c我們能夠理解,因為變數c是其父親環境中的變數,在本環境中找不到變數c時就會去父親環境中尋找。
再來看一個例子:
function createB(){ var result = []; for(var i=0; i<10; i++){ result[i] = function(){ return i; }; } return result; } var result = createB();
我們期望的是執行result[0]()能夠返回0,執行result[1]()能夠返回1,以此類推。可是實際上呢,每個函式返回的都是10.這是因為每個函式的作用域鏈中都儲存著createB()函式的活動物件,所以它們引用的都是同一個變數i。當createB()函式返回後,變數i的值是10,此時每個函式都引用著儲存變數i的同一個變數物件,所以每個函式內部i的值都是10.不過我們可以通過這樣的設定來讓閉包的行為符合我們的預期。
function createB(){ var result = []; for(var i=0; i<10; i++){ result[i] = function(num){ return function(){ return num; }; }(i); } return result; } var result = createB();
(1)在記憶體中維持一個變數。比如前面講的小例子,由於閉包,函式createA中的c會一直存在於記憶體中,因此每次執行func(),都會給變數c加1.
(2)保護函式內的變數安全。還是以最開始的例子,函式createA()中的變數c只有內部的函式才能訪問,而無法通過其他途徑訪問到,因此保護了變數c的安全。
(3)實現物件導向中的物件。javascript並沒有提供類這樣的機制,但是我們可以通過閉包來模擬出類的機制,不同的物件例項擁有獨立的成員和狀態。
這裡我們看一個例子:
var student = function(){ var name = "bing"; var score = 80; return { getName:function(){ return name; }, setName:function(thisName){ name = thisName; } } }();
分別執行下面的語句:
student.name; student.getName(); student.setName("zhongguo"); student.getName();
可以看到,變數student是不能直接訪問變數name的。只能通過getName和setName來對變數name進行讀寫操作。
針對第三點,我們看這樣的一個例子。
function Student(){ var name = "bing"; return { getName:function(){ return name; }, setName:function(thisName){ name = thisName; } } }
分別建立兩個物件stu1,stu2:
var stu1 = Student(); stu1.setName("beijing"); stu1.getName(); var stu2 = Student(); stu2.setName("shanghai"); stu2.getName(); stu1.getName();
輸出的結果分別是:"beijing", "shanghai", "beijing"。可以發現stu1,stu2這兩個物件之間相互獨立,互不影響。
在jQuery中我們經常見這樣的寫法:
(function(x, y){ })(3, 5);
這是立即執行的匿名函式,匿名函式也是一種閉包。我們來看這個:
for(var i=0; i<10; i++) { (function(i){ setTimeout(function(){ console.log(i); },i*1000) })(i); }
這段程式碼是每隔1000ms依次輸出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
如果我們不使用匿名函式,直接在for迴圈裡寫setTimeout會是什麼結果呢,這就回到了第二部分討論的問題,setTimeout也是一個函式呀,他使用了外部環境的變數i,因此每隔1000ms輸出一個10,最後輸出十個10。