前言
函數和作用域啥的我們前面已經了解了,現在就要學習閉包了,這是一個挺晦澀的知識點,初學者可能會感覺不好理解,但是高手都不不以為然了,高手就給我提點意見吧,我和新手一起來學習什么是閉包。
例子
先不說定義,先看一個題,看看大家能得出正確的結果不,
function test(){ var arr = []; for(var i = 0;i<10;i++){ arr[i] = function(){ return i; } } return arr; } var fns = test(); console.log(fns[9]()); // 值是多少? console.log(fns[0]());//值是多少?
結果就是

10 10
你做對了嗎?
什么是閉包
我們知道,javascript中的變量作用域分為全局變量和局部變量,全局的變量我們在什么地方都可以使用,但是局部變量就不是這樣的了,我們只能在該變量的作用域中得到,換句話說就是我們在函數的內部可以使用函數外部的變量,但是我們在函數的外部卻不能使用函數內部定義的局部變量,但是在實際中我們就是想要在函數的外部使用函數內部定義的變量那該怎么辦呢?例子來了
function test(){ var inner = 10; } alert(inner);//error?咋辦
咋辦呢?我們知道,在內部我們可以訪問到這個變量,我們還知道有一個操作符return可以返回想要的值,那我就在內部定義一個函數來訪問這個變量,然后在返回這個函數不就行了,實踐一下
function test(){ var inner = 10; function inFun(){ alert(inner);// }; return inFun; } var outter = test(); outter();//10;
我們做到了,為自己鼓鼓掌,有時候我們就該不斷鼓勵自己一下,不要給自己太大的壓力,我們不是富二代,在不鼓勵一下自己怎么能成為富二代他爹呢。
這就是閉包了,官方沒有給出閉包一個完整的准確的定義,民間流傳的是在一個函數內定義一個函數,並且這個內部函數可以在外面訪問,這時候就形成了閉包。看看上面函數的結構,一個函數返回了一個內部函數,我們知道在正常情況下,一個函數執行結束之后,里面的變量會被釋放,也就是說,在test()這句執行之后,里面的inner應該被釋放了才對,但是我們發現,outter()時我們拿到了inner的值,這就是閉包的特性:如果閉包中使用了局部的變量,那么這個變量會一直貯存在內存中,閉包會一直保持這個值,一直到外部的函數沒有被引用為止,看例子
function closure(){ var num = 0; function add(){ console.log(++num); } return add; } var test1 = closure();//形成一個閉包,保持着自己的一個num變量 test1 ();//1 test1 ();//2 var test2 = closure();//又一個閉包,保持了一個自己的num變量 test2 ();//1 test2 ();//2
好玩不?這就是閉包的神奇的地方,也是讓身為初學者的我們感到彷徨的地方,相信我,我會讓你們理解明白的。要想釋放num占用的內存,就該這樣
test1 = null; test2 = null;
簡單解析下這個例子:在執行 var test1 = closure()時,由於closure()返回到是一個函數,這里就相當於test1變量指向了一個函數add,但是這個add函數有自己的作用域和活動對象,都存在了test1中,執行test1()時,會尋找num變量,由於閉包存儲了該變量就可以直接取到,並且自加1,再一次執行test1()時會繼續在test1執行的add函數的執行環境和作用域中查找,發現num為1了,就找到了這個num;在執行var test2 = closure()時,會重新創建一個閉包,重新存儲執行環境和活動對象,所以這是和第一次完全沒有關系的。
閉包的機制
-
函數也是對象,有[[scope]]屬性(只能通過JavaScript引擎訪問),指向函數定義時的執行環境上下文。
-
假如A是全局的函數,B是A的內部函數。執行A函數時,當前執行環境的上下文指向一個作用域鏈。作用域鏈的第一個對象是當前函數的活動對象(this、參數、局部變量),第二個對象是全局window。
-
當執行代碼運行到B定義地方, 設置函數B的[[scope]]屬性指向執行環境的上下文作用域鏈。
-
執行A函數完畢后,若內部函數B的引用沒外暴,A函數活動對象將被Js垃圾回收處理;反之,則維持,形成閉包。
-
調用函數B時,JavaScript引擎將當前執行環境入棧,生成新的執行環境,新的執行環境的上下文指向一個作用域鏈,由當前活動對象+函數B的[[scope]]組成,鏈的第一個對象是當前函數的活動對象(this、參數、局部變量組成),第二個活動對象是A函數產生的,第三個window。
-
B函數里面訪問一個變量,要進行標志符解析(JavaScript原型也有標識符解析),它從當前上下文指向的作用域鏈的第一個對象開始查找,找不到就查找第二個對象,直到找到相關值就立即返回,如果還沒找到,報undefined錯誤。
-
當有關A函數的外暴的內部引用全部被消除時,A的活動對象才被銷毀。
這段是其他的地方的,就是說了執行環境和作用域的理解閉包怎么維持變量的。
閉包的應用
一個是前面提到的可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中,這既是函數也是弊端。我們可以利用閉包封裝一些私有的屬性,例如
var factorial = (function () { var cache = []; return function (num) { if (!cache[num]) { if (num == 0) { cache[num] = 1; } cache[num] = num * factorial(num - 1); } return cache[num]; } })();
封裝了一個內部私有的屬性來緩存結果。
下面流行的模塊模式,它允許你模擬公共,私有以及特權成員
var Module = (function(){ var privateProperty = 'foo'; function privateMethod(args){ //do something } return { publicProperty: "", publicMethod: function(args){ //do something }, privilegedMethod: function(args){ privateMethod(args); } } })();
另一個類型的閉包叫做立即執行函數表達式,是一個在window上下文中自我調用的匿名函數:
(function(window){ var a = 'foo'; function private(){ // do something } window.Module = { public: function(){ // do something } }; })(this);
閉包的弊端
由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
解釋例子
回到開始的例子,這是閉包的經典的例子,這個和其他的例子和有些不一樣,我們分析一下,這里用了一個數組,其實這里我們執行一次var fns = test(),形成了10個閉包,數組的每一個項存了一個閉包,這與其他的例子是不一樣的,其他的例子是函數執行一次形成了一個閉包,所以這個10個閉包的初始的執行環境是一樣的,每一個閉包使用了i這個變量,這個變量在函數var fns = test()執行之后變為了退出循環的那個i的值10,JavaScript是解釋型的語言,所以在執行數組中的閉包的時,會找到此時i的值10;看看arr的結果
現在想怎樣解決這個問題呢?我們想想,這10個閉包形成時的執行環境和活動對象是一樣的,現在考慮的就是要在初始時就不一樣,我們知道函數的作用域是一層一層的,那我們就需要在這之間家一層作用域,這層作用域要有不同的i的值,我們想到了自執行匿名函數,(funciton(){})(),我們把i的值穿進去,按值傳參就是相當於復制了一份變量嘛,在(funciton(){})()外部的作用域中的i的值的改變不會改變內部的i的值,試一下
function test(){ var arr = []; for(var i = 0;i<10;i++){ (function(i){ arr[i] = function(){return i;}})(i); } return arr; } var fns = test(); console.log(fns[9]()); // 值是9 console.log(fns[0]());//值是0
當然也可以這樣
function test(){ var arr = []; for(var i = 0;i<10;i++){ arr[i] = (function(i){return function(){return i}})(i); } return arr; } var fns = test(); console.log(fns[9]()); // 值是9 console.log(fns[0]());//值是0
這兩個的實質都是在閉包形成之前,給每一個閉包包上一層作用域,在這個作用域中傳一個參數,是每一個閉包上一級的作用域中都有不同的i。當然還有其他的辦法這里不說了。
小結
閉包的應用場景挺多的,在模塊化編程中很重要的,有些地方說函數也是閉包,還是那就話,概念不重要,理解會用才是最現實的。