上一篇我們主要講解了函數的執行過程和原理,本篇我們將介紹函數的另外兩個特殊表現:閉包和立即執行函數。 一 閉包 1, 閉包的形成 之前我們提到,函數執行完畢,馬上就會銷毀自己的AO對象。但是如果遇到下麵這種情況:有子函數的定義,並將子函數返回。它真的就完全銷毀了自己的AO對象嗎? 這將列印什麼呢?表 ...
上一篇我們主要講解了函數的執行過程和原理,本篇我們將介紹函數的另外兩個特殊表現:閉包和立即執行函數。
一 閉包
1, 閉包的形成
之前我們提到,函數執行完畢,馬上就會銷毀自己的AO對象。但是如果遇到下麵這種情況:有子函數的定義,並將子函數返回。它真的就完全銷毀了自己的AO對象嗎?
1 function fn(){ 2 var a = 1; 3 function son(){ 4 console.log(a); 5 } 6 return son; 7 } 8 var test = fn(); 9 test();//error ? 1
這將列印什麼呢?錶面上看,test是son的另一個引用,son內並沒有變數的聲明,consol.log()訪問a應該拋出錯誤。
但事實上,test()將列印1,這是為什麼呢?回憶上一篇文章函數的作用域鏈,不難發現:
當fn執行時:fn.[[scope]] --- {0:AO(fn),1:GO};son被聲明:son.[[scope]] --- {0:AO(fn),1:GO};
renturn son也將保留該屬性,這時fn已經執行完畢:
fn.[[scope]] --- {0:GO}(AO(fn)被銷毀?);
直到test()執行時,test.[[csope]] --- {0:AO(son),1:AO(fn),2:GO}( test是son的另一個引用,實際上他們是同一個函數)。
這時test想要訪問變數a,那麼他將先在自己的AO內查找,沒有,那麼他將到fn的AO里去查找,剛好有,所以最終列印的是1。
這裡被看似已經被fn銷毀的AO(fn),實際上還被son引用著,所以它並沒有真正的被完全銷毀,只是對於fn來說,已經丟棄了對這個對象的引用,看起來像被銷毀了。這個還被son保留著的AO對象我們即稱之為閉包。閉包能幫助一個函數讀取另一個函數內部的變數,它起到了連接兩個函數的橋梁作用。
總結一下,在JavaScripe中形成閉包需要三個要素:
1, 父函數內定義了子函數。
2, 子函數內訪問了父函數的變數。
3, 子函數被返回。
2,閉包的應用
a) 變數私有化,但可以實現全局變數的效果
1 function add(){ 2 var count = 0; 3 return function (){ 4 count ++; 5 //some code 6 console.log(count); 7 } 8 } 9 var myAdd = add(); 10 myAdd();//1 11 myAdd();//2 12 myAdd();//3
b) 用作(類似)緩存
1 function person(){ 2 var money = 0; 3 var obj = { 4 pay:function (){ 5 if(money > 0){ 6 console.log("I spent one yuan."); 7 money --; 8 }else{ 9 console.log("I run out of my money."); 10 } 11 }, 12 make:function (){ 13 console.log("I made one yuan."); 14 money ++; 15 } 16 }; 17 return obj; 18 } 19 var person1 = person(); 20 person1.pay();//"I run out of my money." 21 person1.make();//"I made one yuan." 22 person1.pay();//"I spent one yuan."
c) 模塊化開發,防止變數污染
1 var a = "Global"; 2 function p0(){ 3 console.log(a); 4 } 5 function p1(){ 6 var a = "p1"; 7 return function(){ 8 console.log(a); 9 }; 10 } 11 function p2(){ 12 var a = "p2"; 13 return function(){ 14 console.log(a); 15 }; 16 } 17 var myP1 = p1(); 18 var myP2 = p2(); 19 20 p0();//"Global" 21 myP1();//"p1" 22 myP2();//"p2"
大型項目一般都是多人協同開發,每個人負責不同的模塊,不可避免的,大家可能使用了相同的變數名,這將造成全局變數污染。使用閉包,即可解決這個問題題。瞭解了下一節的立即執行函數,這段代碼還可以加以優化。
二 立即執行函數
在認識立即執行函數之前,讓我們先來瞭解執行符()的兩個特點。
1)只有表達式才能被()執行。
1 function test(){ 2 console.log(1); 3 }();//error 這是函數聲明 4 var test = function (){ 5 console.log(1); 6 }();//1 這是函數表達式
2)能被()執行的表達式會被系統忽略函數名稱。
1 var test = function (){ 2 console.log(1); 3 }();//1 4 console.log(test);//undefined 5 //這是一個有趣的現象:我們聲明瞭變數test,並把一個函數賦值給它,緊接著使用()執行了這個表達式,隨即列印出了1。
按理說,這時test的值應該是一個匿名函數的函數體才對,但實際上它是undefined,變數剛被聲明的狀態,即系統放棄了變數test對函數的引用。
這很好的印證了()執行符的第二個特點。
1,立即執行函數的形式
我們知道"()"括弧實際上也是一種數學運算符,表示運算優先順序的。那麼我們當然可以把函數聲明用括弧包起來,使它成為一個表達式。這樣我們就可以使用()執行符馬上執行它並得到函數執行的結果了。
1 (function test(){ 2 console.log(1); 3 }());//1 4 //集合()執行符的第二個特點,我們還可以將它優化 5 (function (){ 6 console.log(1); 7 });//1
以上就是立即執行函數的最終形式。另外,把()執行符放在函數聲明的括弧外面其實也是可以的。
1 (function (){ 2 console.log(1); 3 })();
2,立即執行函數的特點
知道了()執行符的特點,其實我們不難發現:
1)立即執行函數被聲明後會馬上執行函數體內的代碼。
2)執行完畢後立即銷毀,不會被一直保存在記憶體中。
3)只能被執行一次,不能起到代碼塊復用的功能。
除了上述特點外,立即執行函數和普通函數的功能完全一樣。
3,立即執行函數結合閉包的經典應用
1 function fn(){ 2 var arr = []; 3 for(var i = 0; i < 10; i++){ 4 arr[i] = function () { 5 console.log(i); 6 }; 7 } 8 return arr; 9 } 10 var myArr = fn(); 11 myArr.map(function (item){ 12 item(); 13 });//10 10 10 10 10 10 10 10 10 10
我們是想依次輸出1--9啊!為什麼跑出來10個10呢?
仔細想一想,不難發現,這是因為所有子函數和fn形成的是同一個閉包,所以最後都列印了10,那麼要怎樣才能實現我們想要的功能呢?
1 function fn(){ 2 var arr = []; 3 for(var i = 0; i < 10; i++){ 4 (function (j){ 5 arr[j] = function () { 6 console.log(j); 7 } 8 }(i)); 9 } 10 return arr; 11 } 12 var myArr = fn(); 13 myArr.map(function (item){ 14 item(); 15 });//0 1 2 3 4 5 6 7 8 9
通過利用立即執行函數定義完即被執行的特點,使每個子函數都和fn形成單獨的閉包,再把每次迴圈的i的值當做它的實參傳遞進去,那麼最終子函數在執行時訪問到的其實都是各自閉包里的i的值了。這樣就得到了我們想要的結果了。
雖然閉包能在很多地方發揮很大作用,但閉包也有它自身的缺陷:閉包將一直占用記憶體空間,嚴重時將導致記憶體泄漏,甚至系統崩潰。所以我們應該儘量避免使用閉包,如果別無他法,也應該在使用完後手動的解除它對記憶體的占用,比如把引用返回函數的變數賦值為null。