起點(diǎn)
本文之所以會(huì)寫這種老生常談的文章眠屎,是為了接下來的設(shè)計(jì)模式做鋪墊。既然已經(jīng)提筆了肆饶,就打算不改了改衩,繼續(xù)寫下去,相信也一定有很多人對(duì)閉包這樣的概念有些模糊驯镊,那就瞧一瞧葫督、看一看
畢竟閉包和高階函數(shù)這兩種概念,在開發(fā)中是非常有分量的板惑。好處多多橄镜,妙處多多,那么我們就不再兜圈子了冯乘,直接開始今天的主題洽胶,閉包&高階函數(shù)
閉包
閉包是前端er離不開的一個(gè)話題,而且也是一個(gè)難懂又必須明白的概念裆馒。說起閉包姊氓,它與變量的作用域和變量的生命周期密切相關(guān)。
這兩個(gè)知識(shí)點(diǎn)我們也無法繞開喷好,那么就一起了解下吧
變量作用域
首先變量作用域分為兩類:全局作用域和局部作用域他膳,這個(gè)沒話說大家都懂。我們常說的變量作用域其實(shí)也主要是在函數(shù)中聲明的作用域
在函數(shù)中聲明變量時(shí)沒有var關(guān)鍵字绒窑,就代表是全局變量
在函數(shù)中聲明變量帶有var關(guān)鍵字的即是局部變量,局部變量只能在函數(shù)內(nèi)才能訪問到
function fn() {
var a = 110; // a為局部變量
console.log(a); // 110
}
fn();
console.log(a); // a is not defined 外部訪問不到內(nèi)部的變量
上面代碼展示了在函數(shù)中聲明的局部變量a在函數(shù)外部確實(shí)無法拿到舔亭。小樣兒的還挺囂張些膨,對(duì)于迎難而上的coder來說,還不信拿不下a了
客官钦铺,莫急订雾,且聽風(fēng)吟。大家是否還記得在js中矛洞,函數(shù)可是“一等公民”啊洼哎,大大滴厲害
函數(shù)可以創(chuàng)造函數(shù)作用域,在函數(shù)作用域中如果要查找一個(gè)變量的時(shí)候沼本,如果在該函數(shù)內(nèi)沒有聲明這個(gè)變量噩峦,就會(huì)向該函數(shù)的外層繼續(xù)查找,一直查到全局變量為止
所以變量的查找是由內(nèi)而外的抽兆,這也形成了所謂的作用域鏈
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(); // 調(diào)用函數(shù)
利用作用域鏈识补,我們?cè)囍ツ玫絘,改造一下fn函數(shù)
function fn() {
var a = 110; // a為局部變量
return function() {
console.log(a);
}
console.log(a); // 110
}
var fn2 = fn();
fn2(); // 110
如此這般辫红,這般如此凭涂,輕而易舉祝辣,小case的事,就可以從外面訪問到局部變量a了
那么到此為止切油,我們已經(jīng)發(fā)現(xiàn)了閉包的其中一個(gè)意義:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)蝙斜,嗯,沒毛病澎胡,繼續(xù)往下看
變量生命周期
在解決了上面如何拿到小樣兒a的問題孕荠,我們不妨再把變量生命周期這個(gè)概念先簡(jiǎn)單地過一遍。
對(duì)于全局變量來說滤馍,它的生命周期自然是永久的(forever)岛琼,除非我們不高興,主動(dòng)干掉它巢株,報(bào)銷它槐瑞。
而對(duì)于在函數(shù)中通過var聲明的局部變量來說,就沒那么幸運(yùn)了阁苞,當(dāng)函數(shù)執(zhí)行完畢困檩,局部變量們就失去了價(jià)值,就被垃圾回收機(jī)制給當(dāng)成垃圾處理掉了
比如像下面這樣的代碼就很可憐
function fn() {
var a = 123; // fn執(zhí)行完畢后那槽,變量a就將被銷毀了
console.log(a);
}
fn();
雖然以上垃圾回收的過程我們無法親眼看見悼沿,但是聽者傷心聞?wù)吡鳒I啊∩Ь模可不可以不要如此殘忍糟趾,我愿傾其所有,換你三生三世甚牲。
悲傷的到來义郑,我們無法拒絕,那就讓我們想辦法去改變這一切≌筛疲現(xiàn)在再讓我們來看下這段代碼:
function add() {
var a = 1;
return function() {
a++;
console.log(a);
}
}
var fn = add();
fn(); // 2
fn(); // 3
fn(); // 4
這段代碼最神奇的地方就是非驮,當(dāng)add函數(shù)執(zhí)行完后,局部變量a并沒有被銷毀雏赦,而是依然存在劫笙,這其中到底發(fā)生了什么?讓我們慢慢分析一下:
當(dāng)fn = add()時(shí)星岗,fn返回了一個(gè)函數(shù)的引用填大,這個(gè)函數(shù)里有局部變量a
既然這個(gè)局部變量還能被外部訪問fn(),就沒有必要把它給銷毀了俏橘,于是就保留了下來
閉包是個(gè)好東西栋盹,可以完成很多工作,其中就包括一道網(wǎng)上常考的經(jīng)典題目
<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>
見過這道題的觀眾請(qǐng)舉手例获,確實(shí)這道題的目的就是為了考對(duì)閉包的理解汉额。上面的答案無論怎么點(diǎn)結(jié)果都是4。
這是因?yàn)閘i節(jié)點(diǎn)的onclick事件屬于異步的榨汤,在click被觸發(fā)的時(shí)候蠕搜,for循環(huán)以迅雷不及掩耳盜鈴的速度就執(zhí)行完畢了,此時(shí)變量i的值已經(jīng)是4了
因此在li的click事件函數(shù)順著作用域鏈從內(nèi)向外開始找i的時(shí)候收壕,發(fā)現(xiàn)i的值已經(jīng)全是4了
解決方法就需要通過閉包妓灌,把每次循環(huán)的i值都存下來。然后當(dāng)click事件繼續(xù)順著作用域鏈查找的時(shí)候蜜宪,會(huì)先找到被存下來的i虫埂,這樣每一個(gè)li點(diǎn)擊都可以找到對(duì)應(yīng)的i值了
<script>
var aLi = document.getElementsByTagName('li');
for (var i = 0; i < aLi.length; i++) {
(function(n) { // n為對(duì)應(yīng)的索引值
aLi[i].onclick = function() {
console.log(n); // 0, 1, 2, 3
};
})(i); // 這里i每循環(huán)一次都存一下,然后把0,1,2,3傳給上面的形參n
}
</script>
其他作用
閉包應(yīng)用非常廣泛圃验,我們這里就說一下大家熟知的掉伏,比如可以封裝私有變量,可以把一些不需要暴露在全局的變量封裝成私有變量澳窑,這樣可以防止造成變量的全局污染
var sum = (function() {
var cache = {}; // 將cache放入函數(shù)內(nèi)部斧散,避免被其他地方修改
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() {};
// 最后把想要暴露出去的內(nèi)容可以掛載到window上
win.obj = obj;
})(window);
是的,沒錯(cuò)摊聋,利用閉包也可以做到模塊化挣磨。另外還可以將變量的使用延長(zhǎng)榜聂,再來看一個(gè)例子
var monitor = (function() {
var imgs = [];
return function(src){
var img = new Image();
imgs.push(img);
img.src = src;
}
})();
monitor('http://dd.com/srp.gif');
上面的代碼是用于打點(diǎn)進(jìn)行統(tǒng)計(jì)數(shù)據(jù)的情形脾猛,在之前的一些瀏覽器中嗤锉,會(huì)出現(xiàn)打點(diǎn)丟失的情況,因?yàn)閕mg是函數(shù)內(nèi)的局部變量煎源,當(dāng)函數(shù)執(zhí)行完后img就被銷毀了鹿寨,而此時(shí)可能http請(qǐng)求還沒有發(fā)出。
所以遇到這種情況的時(shí)候薪夕,把img變量用閉包封裝起來,就可以解決了
內(nèi)存管理
很多人都聽過一個(gè)版本赫悄,就是閉包會(huì)造成內(nèi)存泄漏原献,所以要盡量減少閉包的使用
Just now就來為閉包來正名,不是你想象那樣的:
局部變量本來應(yīng)該隨著函數(shù)的執(zhí)行完畢被銷毀埂淮,但如果局部變量被封裝在閉包形成的環(huán)境中姑隅,那這個(gè)局部變量就一直能存在。從我們上面實(shí)踐得出的結(jié)果來看倔撞,這話說的沒毛病
But之所以使用閉包是因?yàn)槲覀兿胍岩恍┳兞看嫫饋矸奖阋院笫褂媒惭觯@和放到全局下,對(duì)內(nèi)存的影響是一致的痪蝇,并不算是內(nèi)存泄漏鄙陡。如果在將來想回收這些變量冕房,直接把變量設(shè)為null即可了
還有就是在使用閉包的同時(shí)比較容易形成循環(huán)引用,如果閉包的作用域鏈中保存著一些DOM節(jié)點(diǎn)趁矾,此時(shí)就有可能造成內(nèi)存泄漏耙册。但這本身并非閉包的問題,也并非js的問題
要怪就怪老版本的IE同志吧毫捣,它內(nèi)部實(shí)現(xiàn)的垃圾回收機(jī)制采用的是引用計(jì)數(shù)策略详拙。在老同志IE中,如果兩個(gè)對(duì)象之間形成了循環(huán)引用蔓同,那么這兩個(gè)對(duì)象都不能被回收饶辙,但循環(huán)引用造成的內(nèi)存泄漏其本質(zhì)也不是閉包的錯(cuò)
同樣要解決循環(huán)引用代理的內(nèi)存泄漏問題,只需把循環(huán)引用中的變量設(shè)為null就好
上面就是我們替閉包的正名斑粱,閉包也不容易弃揽,被人用還不討好。它明白珊佣,不是它的鍋蹋宦,它是不需要背的!