起點
本文之所以會寫這種老生常談的文章,是為了接下來的設計模式做鋪墊。既然已經提筆了,就打算不改了,繼續寫下去,相信也一定有很多人對閉包這樣的概念有些模糊,那就瞧一瞧、看一看
畢竟閉包和高階函式這兩種概念,在開發中是非常有分量的。好處多多,妙處多多,那麼我們就不再兜圈子了,直接開始今天的主題,閉包&高階函式
閉包
閉包是前端er離不開的一個話題,而且也是一個難懂又必須明白的概念。說起閉包,它與變數的作用域和變數的生命週期密切相關。 這兩個知識點我們也無法繞開,那麼就一起了解下吧
變數作用域
首先變數作用域分為兩類:全域性作用域和區域性作用域,這個沒話說大家都懂。我們常說的變數作用域其實也主要是在函式中宣告的作用域
- 在函式中宣告變數時沒有var關鍵字,就代表是全域性變數
- 在函式中宣告變數帶有var關鍵字的即是區域性變數,區域性變數只能在函式內才能訪問到
function fn() {
var a = 110; // a為區域性變數
console.log(a); // 110
}
fn();
console.log(a); // a is not defined 外部訪問不到內部的變數
複製程式碼
上面程式碼展示了在函式中宣告的區域性變數a在函式外部確實無法拿到。小樣兒的還挺囂張,對於迎難而上的coder來說,還不信拿不下a了
客官,莫急,且聽風吟。大家是否還記得在js中,函式可是“一等公民”啊,大大滴厲害
函式可以創造函式作用域,在函式作用域中如果要查詢一個變數的時候,如果在該函式內沒有宣告這個變數,就會向該函式的外層繼續查詢,一直查到全域性變數為止
所以變數的查詢是由內而外的,這也形成了所謂的作用域鏈
var a = 7;
function outer() {
var b = 9;
function inner() {
var c = 8;
alert(b);
alert(a);
}
inner();
alert(c); // c is not defined
}
outer(); // 呼叫函式
複製程式碼
利用作用域鏈,我們試著去拿到a,改造一下fn函式
function fn() {
var a = 110; // a為區域性變數
return function() {
console.log(a);
}
console.log(a); // 110
}
var fn2 = fn();
fn2(); // 110
複製程式碼
如此這般,這般如此,輕而易舉,小case的事,就可以從外面訪問到區域性變數a了
那麼到此為止,我們已經發現了閉包的其中一個意義:閉包就是能夠讀取其他函式內部變數的函式,嗯,沒毛病,繼續往下看
變數生命週期
在解決了上面如何拿到小樣兒a的問題,我們不妨再把變數生命週期這個概念先簡單地過一遍。
- 對於全域性變數來說,它的生命週期自然是永久的(forever),除非我們不高興,主動幹掉它,報銷它。
- 而對於在函式中通過var宣告的區域性變數來說,就沒那麼幸運了,當函式執行完畢,區域性變數們就失去了價值,就被垃圾回收機制給當成垃圾處理掉了
- 比如像下面這樣的程式碼就很可憐
function fn() {
var a = 123; // fn執行完畢後,變數a就將被銷燬了
console.log(a);
}
fn();
複製程式碼
雖然以上垃圾回收的過程我們無法親眼看見,但是聽者傷心聞者流淚啊。可不可以不要如此殘忍,我願傾其所有,換你三生三世。
悲傷的到來,我們無法拒絕,那就讓我們想辦法去改變這一切。現在再讓我們來看下這段程式碼:
function add() {
var a = 1;
return function() {
a++;
console.log(a);
}
}
var fn = add();
fn(); // 2
fn(); // 3
fn(); // 4
複製程式碼
這段程式碼最神奇的地方就是,當add函式執行完後,區域性變數a並沒有被銷燬,而是依然存在,這其中到底發生了什麼?讓我們慢慢分析一下:
- 當fn = add()時,fn返回了一個函式的引用,這個函式裡有區域性變數a
- 既然這個區域性變數還能被外部訪問fn(),就沒有必要把它給銷燬了,於是就保留了下來
閉包是個好東西,可以完成很多工作,其中就包括一道網上常考的經典題目
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
var aLi = document.getElementsByTagName('li');
for (var i = 0; i < aLi.length; i++) {
aLi[i].onclick = function() {
console.log(i); // ?
};
}
</script>
複製程式碼
見過這道題的觀眾請舉手,確實這道題的目的就是為了考對閉包的理解。上面的答案無論怎麼點結果都是4。
這是因為li節點的onclick事件屬於非同步的,在click被觸發的時候,for迴圈以迅雷不及掩耳盜鈴的速度就執行完畢了,此時變數i的值已經是4了
因此在li的click事件函式順著作用域鏈從內向外開始找i的時候,發現i的值已經全是4了
解決方法就需要通過閉包,把每次迴圈的i值都存下來。然後當click事件繼續順著作用域鏈查詢的時候,會先找到被存下來的i,這樣每一個li點選都可以找到對應的i值了
<script>
var aLi = document.getElementsByTagName('li');
for (var i = 0; i < aLi.length; i++) {
(function(n) { // n為對應的索引值
aLi[i].onclick = function() {
console.log(n); // 0, 1, 2, 3
};
})(i); // 這裡i每迴圈一次都存一下,然後把0,1,2,3傳給上面的形參n
}
</script>
複製程式碼
其他作用
閉包應用非常廣泛,我們這裡就說一下大家熟知的,比如可以封裝私有變數,可以把一些不需要暴露在全域性的變數封裝成私有變數,這樣可以防止造成變數的全域性汙染
var sum = (function() {
var cache = {}; // 將cache放入函式內部,避免被其他地方修改
return function() {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
var a = 0;
for (var i = 0; i < arguments.length; i++) {
a += arguments[i];
}
return cache[args] = a;
}
})();
複製程式碼
除此之外相信很多人都見過一些庫如jQuery,underscore他們的最外層都是類似如下樣子的程式碼
(function(win, undefined) {
var a = 1;
var obj = {};
obj.fn = function() {};
// 最後把想要暴露出去的內容可以掛載到window上
win.obj = obj;
})(window);
複製程式碼
是的,沒錯,利用閉包也可以做到模組化。另外還可以將變數的使用延長,再來看一個例子
var monitor = (function() {
var imgs = [];
return function(src){
var img = new Image();
imgs.push(img);
img.src = src;
}
})();
monitor('http://dd.com/srp.gif');
複製程式碼
上面的程式碼是用於打點進行統計資料的情形,在之前的一些瀏覽器中,會出現打點丟失的情況,因為img是函式內的區域性變數,當函式執行完後img就被銷燬了,而此時可能http請求還沒有發出。
所以遇到這種情況的時候,把img變數用閉包封裝起來,就可以解決了
記憶體管理
很多人都聽過一個版本,就是閉包會造成記憶體洩漏,所以要儘量減少閉包的使用
Just now就來為閉包來正名,不是你想象那樣的:
- 區域性變數本來應該隨著函式的執行完畢被銷燬,但如果區域性變數被封裝在閉包形成的環境中,那這個區域性變數就一直能存在。從我們上面實踐得出的結果來看,這話說的沒毛病
- But之所以使用閉包是因為我們想要把一些變數存起來方便以後使用,這和放到全域性下,對記憶體的影響是一致的,並不算是記憶體洩漏。如果在將來想回收這些變數,直接把變數設為null即可了
- 還有就是在使用閉包的同時比較容易形成迴圈引用,如果閉包的作用域鏈中儲存著一些DOM節點,此時就有可能造成記憶體洩漏。但這本身並非閉包的問題,也並非js的問題
- 要怪就怪老版本的IE同志吧,它內部實現的垃圾回收機制採用的是引用計數策略。在老同志IE中,如果兩個物件之間形成了迴圈引用,那麼這兩個物件都不能被回收,但迴圈引用造成的記憶體洩漏其本質也不是閉包的錯
- 同樣要解決迴圈引用代理的記憶體洩漏問題,只需把迴圈引用中的變數設為null就好
上面就是我們替閉包的正名,閉包也不容易,被人用還不討好。它明白,不是它的鍋,它是不需要背的!
這不是終點
雖然不是終點,但還是要搞個總結性發炎的,不然怎麼對得起扁桃體兄
閉包:
- 是一個能夠讀取其他函式內部變數的函式,實質上是變數的解析過程(由內而外)
- 可以用來封裝私有變數,實現模組化
- 可以儲存變數到記憶體中
說完了閉包,我們先休息個一分鐘,稍微理理思路。
然而真正的原因是由於文章內容過長,拆分成了姊妹篇
So還請客官移步,小憩片刻之後,來繼續進入下一個主題,高階函式