全面理解JavaScript中的閉包,理解javascript
引子
閉包是有權訪問另一個函數範圍中的變數的函數。
閉包是javascript中很難理解的部分,很多進階的應用都依靠閉包來實現的,我們先來看下面的一個例子:
function outer() { var i = 100; function inner() { console.log(i); }}
上面代碼,根據變數的範圍,函數outer中所有的局部變數,對函數inner都是可見的;函數inner中的局部變數,在函數inner外是不可見的,所以在函數inner外是無法讀取函數inner的局部變數的。
既然函數inner可以讀取函數outer的局部變數,那麼只要將inner作為返會值,就可以直接在ouer外部讀取inner的局部變數。
function outer() { var i = 100; function inner() { console.log(i); } return inner;}var rs = outer();rs();
這個函數有兩個特點:
- 函數inner嵌套在函數ouer內部;
- 函數outer返回函數inner。
這樣執行完var rs = outer()後,實際rs指向了函數inner。這段代碼其實就是一個閉包。也就是說當函數outer內的函數inner被函數outer外的一個變數引用的時候,就建立了一個閉包。
範圍
簡單的說,範圍就是變數與函數的可存取範圍,即範圍控制著變數與函數的可見度和生命週期。在JavaScript中,變數的範圍有全域範圍和局部範圍兩種。
全域範圍
var num1 = 1;function fun1 (){ num2 = 2;}
以上三個對象num1,num2和fun1均是全域範圍,這裡要注意的是末定義直接賦值的變數自動聲明為擁有全域範圍;
局部範圍
function wrap(){ var obj = "我被wrap包裹起來了,wrap外部無法直接存取到我"; function innerFun(){ //外部無法訪問我 }}
範圍鏈
Javascript中一切皆對象,這些對象有一個[[Scope]]屬性,該屬性包含了函數被建立的範圍中對象的集合,這個集合被稱為函數的範圍鏈(Scope Chain),它決定了哪些資料能被函數訪問。
function add(a,b){ return a+b;}
當函數建立的時候,它的[[scope]]屬性自動添加好全域範圍
var sum = add(3,4);
當函數調用的時候,會建立一個稱為運行期上下文(execution context)的內部對象,z這個對象定義了函數執行時的環境。它也有自己的範圍鏈,用於標識符解析,而它的範圍鏈初始化為當前運行函數的[[Scope]]所包含的對象。
在函數執行過程中,每遇到一個變數,都會經曆一次標識符解析過程以決定從哪裡擷取和儲存資料。該過程從範圍鏈頭部,也就是從使用中的物件開始搜尋,尋找同名的標識符,如果找到了就使用這個標識符對應的變數,如果沒找到繼續搜尋範圍鏈中的下一個對象,如果搜尋完所有對象(最後一個為全域對象)都未找到,則認為該標識符未定義。
閉包
閉包簡單來說就是一個函數訪問了它的外部變數。
var quo = function(status){ return { getStatus: function(){ return status; } }}
status儲存在quo中,它返回了一個對象,這個對象裡的方法getStatus引用了這個status變數,即getStatus函數訪問它的外部變數status;
var newValue = quo('string');//返回了一個匿名對象,被newValue引用著newValue.getStatus();//訪問到了quo的內部變數status
假如並沒有getStatus這個方法,那麼quo('sting')結束後,status自動被回收,正是因為返回的匿名對象被一個全域對象引用,那麼這個匿名對象又依賴於status,所以會阻止status的釋放。
例子:
//錯誤方案var test = function(nodes){ var i ; for(i = 0;i<nodes.length;i++){ nodes[i].onclick = function(e){ alert(i); } }}
匿名函數建立了一個閉包,那麼其訪問的i是外部test函數中的i,所以每一個節點實際上引用的是同一個i。
//改進方案var test = function(nodes){ var i ; for(i = 0;i<nodes.length;i++){ nodes[i].onclick = function(i){ return function(){ alert(i); }; }(i); }}
每一個節點綁定了一個事件,這個事件接收一個參數,並且立即運行,傳入i,因為是按值傳遞的,所以每一次迴圈都會為當前i產生一個新的備份。
閉包的作用
function outer() { var i = 100; function inner() { console.log(i++); } return inner;}var rs = outer();rs(); //100rs(); //101rs(); //102
上面的代碼中,rs是閉包inner函數。rs共運行了三次,第一次100,第二次101,第三次102,這說明在函數outer中的局部變數i一直儲存在記憶體中,並沒有在調用自動清除。
閉包的作用就是在outer執行完畢並返回後,閉包使javascript的記憶體回收機制(grabage collection)不會回收outer所佔的記憶體,因為outer的內建函式inner的執行要依賴outer中的變數。(另一種解釋:outer是inner的父函數,inner被賦給了一個全域變數,導致inner會一直在記憶體中,而inner的存在依賴於outer,因些outer也始終於在記憶體中,不會在調用結束後被垃圾收集回收)。
閉包有權訪問函數內部的所有變數。
當函數返回一個閉包時,這個函數的範圍將會一直在記憶體中儲存到閉包不存在為止。
閉包與變數
由於範圍鏈的機制,閉包只能取得包含函數中任何變數的最後一個值。看下面例子:
function f() { var rs = []; for (var i=0; i <10; i++) { rs[i] = function() { return i; }; } return rs;}var fn = f();for (var i = 0; i < fn.length; i++) { console.log('函數fn[' + i + ']()傳回值:' + fn[i]());}
函數會返回一個數組,表面上看,似乎每個函數都應該返回自己的索引值,實際上,每個函數都返回10,這是因為第個函數的範圍鏈上都儲存著函數f的使用中的物件,它們引用的都是同一變數i。當函數f返回後,變數i的值為10,此時每個函數都儲存著變數i的同一個變數對象。我們可以通過建立另一個匿名函數來強制讓閉包的行為符合預期。
function f() { var rs = []; for (var i=0; i <10; i++) { rs[i] = function(num) { return function() { return num; }; }(i); } return rs;}var fn = f();for (var i = 0; i < fn.length; i++) { console.log('函數fn[' + i + ']()傳回值:' + fn[i]());}
這個版本中,我們沒有直接將閉包賦值給數組,而是定義了一個匿名函數,並將立即執行匿名函數的結果賦值給數組。這裡匿名函數有一個參數num,在調用每個函數時,我們傳入變數i,由於參數是按值傳遞的,所以就會將變數i複製給參數num。而在這個匿名函數內部,又建立了並返回了一個訪問num的閉包,這樣,rs數組中每個函數都有自己num變數的一個副本,因此就可以返回不同的數值了。
閉包中的this對象
var name = 'Jack';var o = { name : 'bingdian', getName : function() { return function() { return this.name; }; }}console.log(o.getName()()); //Jackvar name = 'Jack';var o = { name : 'bingdian', getName : function() { var self = this; return function() { return self.name; }; }}console.log(o.getName()()); //bingdian
記憶體泄露
function assignHandler() { var el = document.getElementById('demo'); el.onclick = function() { console.log(el.id); }}assignHandler();
以上代碼建立了作為el元素事件處理常式的閉包,而這個閉包又建立了一個循環參考,只要匿名函數存在,el的引用數至少為1,因些它所佔用的記憶體就永完不會被回收。
function assignHandler() { var el = document.getElementById('demo'); var id = el.id; el.onclick = function() { console.log(id); } el = null;}assignHandler();
把變數el設定null能夠解除DOM對象的引用,確保正常回收其佔用記憶體。
模仿塊級範圍
任何一對花括弧({和})中的語句集都屬於一個塊,在這之中定義的所有變數在代碼塊外都是不可見的,我們稱之為塊級範圍。
(function(){ //塊級範圍})();
閉包的應用
保護函數內的變數安全。如前面的例子,函數outer中i只有函數inner才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。
在記憶體中維持一個變數。如前面的例子,由於閉包,函數outer中i的一直存在於記憶體中,因此每次執行rs(),都會給i加1。