封閉了內(nèi)心卻包容了天下邻眷,閉包你并不孤獨(dú)

起點(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就好

上面就是我們替閉包的正名斑粱,閉包也不容易弃揽,被人用還不討好。它明白珊佣,不是它的鍋蹋宦,它是不需要背的!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咒锻,一起剝皮案震驚了整個(gè)濱河市冷冗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惑艇,老刑警劉巖蒿辙,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異滨巴,居然都是意外死亡思灌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門恭取,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泰偿,“玉大人,你說我怎么就攤上這事蜈垮『孽耍” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵攒发,是天一觀的道長(zhǎng)调塌。 經(jīng)常有香客問我,道長(zhǎng)惠猿,這世上最難降的妖魔是什么羔砾? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上姜凄,老公的妹妹穿的比我還像新娘政溃。我一直安慰自己,他們只是感情好檀葛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布玩祟。 她就那樣靜靜地躺著,像睡著了一般屿聋。 火紅的嫁衣襯著肌膚如雪空扎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天润讥,我揣著相機(jī)與錄音转锈,去河邊找鬼。 笑死楚殿,一個(gè)胖子當(dāng)著我的面吹牛撮慨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脆粥,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼砌溺,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了变隔?” 一聲冷哼從身側(cè)響起规伐,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匣缘,沒想到半個(gè)月后猖闪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肌厨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年培慌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柑爸。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吵护,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出表鳍,到底是詐尸還是另有隱情馅而,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布进胯,位于F島的核電站,受9級(jí)特大地震影響原押,放射性物質(zhì)發(fā)生泄漏胁镐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盯漂。 院中可真熱鬧颇玷,春花似錦、人聲如沸就缆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竭宰。三九已至空郊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間切揭,已是汗流浹背狞甚。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留廓旬,地道東北人哼审。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像孕豹,于是被迫代替她去往敵國和親涩盾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容