關(guān)于js閉包

本文首發(fā)于我的博客這是我的github,歡迎來訪奕枝。

??閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的常見方式瓶堕,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)隘道。
??之所以一個(gè)內(nèi)部的函數(shù)可以訪問其外部的變量,而且在其被返回或是調(diào)用時(shí)還可以訪問郎笆,是因?yàn)檫@個(gè)內(nèi)部函數(shù)的作用域鏈中包含外部函數(shù)的作用域谭梗。

知識(shí)儲(chǔ)備

在了解閉包之前,先要熟悉以下幾點(diǎn):
??1. 首先要理解執(zhí)行環(huán)境宛蚓,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù)激捏。
??2. 每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對(duì)象,環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中凄吏。
??3. 每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境远舅,當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)時(shí)闰蛔,函數(shù)的環(huán)境就會(huì)被推入到一個(gè)環(huán)境棧中。而在函數(shù)執(zhí)行之后图柏,棧將其環(huán)境彈出序六,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
??4. 當(dāng)某個(gè)函數(shù)被調(diào)用時(shí)蚤吹,會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境及其相應(yīng)的作用域鏈例诀。然后使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動(dòng)對(duì)象。在函數(shù)中裁着,活動(dòng)對(duì)象作為變量對(duì)象使用(作用域鏈?zhǔn)怯擅繉拥淖兞繉?duì)象鏈起來的)繁涂。
??5. 在作用域鏈中,外部函數(shù)的活動(dòng)對(duì)象始終處于第二位跨算,外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位爆土,直到作用域鏈終點(diǎn)即全局執(zhí)行環(huán)境。
??6. 作用域鏈的本質(zhì)是一個(gè)指向變量對(duì)象的指針列表诸蚕,它只引用但不實(shí)際包含變量對(duì)象步势。


1.一般情況下

??不談?wù)撻]包,一般的背犯,從在全局執(zhí)行環(huán)境創(chuàng)建一個(gè)函數(shù)開始坏瘩。
??在創(chuàng)建一個(gè)函數(shù)時(shí),會(huì)創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈漠魏,這個(gè)作用域鏈被保存在函數(shù)內(nèi)部的[[Scope]]倔矾。
??然后執(zhí)行流進(jìn)入這個(gè)函數(shù),函數(shù)的執(zhí)行環(huán)境被壓入環(huán)境棧中柱锹,此函數(shù)執(zhí)行環(huán)境的活動(dòng)對(duì)象作為變量對(duì)象被創(chuàng)建并推入執(zhí)行環(huán)境作用域鏈的前端哪自。
??對(duì)這個(gè)例子中的函數(shù)而言,其作用域鏈中包含兩個(gè)變量對(duì)象:本地活動(dòng)對(duì)象和全局變量對(duì)象禁熏。
??無論在什么時(shí)候在函數(shù)中訪問變量時(shí)壤巷,會(huì)從作用域鏈搜索變量名。
??一般情況下瞧毙,函數(shù)執(zhí)行完胧华,局部活動(dòng)對(duì)象就會(huì)被銷毀,內(nèi)存中僅有全局作用域(里邊只有全局執(zhí)行環(huán)境的變量對(duì)象)宙彪。
??以下面這段代碼為例:

function compare (value1, value2) {         
//創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈矩动,保存在[[Scope]]
    if (value1 < value2) {
//訪問函數(shù)變量時(shí),即在代碼最后一條語(yǔ)句執(zhí)行過程中释漆,會(huì)從作用域鏈前端開始搜索變量名
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);
//執(zhí)行流進(jìn)入函數(shù)時(shí)悲没,compare的執(zhí)行環(huán)境壓入環(huán)境棧
//compare執(zhí)行環(huán)境的活動(dòng)對(duì)象作為變量對(duì)象接到作用域鏈的前端
//函數(shù)執(zhí)行完,compare執(zhí)行環(huán)境彈出棧灵汪,compare活動(dòng)對(duì)象銷毀

如圖檀训,作用域鏈從0開始向后查找:


閉包1.1

2.產(chǎn)生閉包的情況下

如下是一個(gè)以屬性名作為參數(shù)柑潦,按其屬性的值對(duì)數(shù)據(jù)進(jìn)行排序的函數(shù):

function createComparisonFunction(propertyName) {     
    return function(object1,object2){         //返回一個(gè)匿名函數(shù)
        var value1=object1[propertyName];
        var value2=object2[propertyName];
        if(value1<value2){
            return -1;
        } else if (value1>value2){
            return 1;
        } else {
            return 0;
        }
    };
}
var data=[{name:"Zachary",age:28},{name:"Nicholas",age:29}];
data.sort(createComparisonFunction("name"));
console.log(data[0]);           //Object {name: "Nicholas", age: 29}
data.sort(createComparisonFunction("age"));
console.log(data[0]);           //Object {name: "Zachary", age: 28}

createComparisonFunction()函數(shù)和返回的匿名函數(shù)的作用域鏈如下圖所示:

閉包2.1

??在匿名函數(shù)從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數(shù)的活動(dòng)對(duì)象和全局變量對(duì)象峻凫。這樣渗鬼,匿名函數(shù)就可以訪問在createComparisonFunction()中定義的所有變量。

更為重要的是:

??createComparisonFunction()函數(shù)在執(zhí)行完畢后荧琼,其他活動(dòng)對(duì)象也不會(huì)被銷毀譬胎,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對(duì)象。
??當(dāng)createComparisonFunction()函數(shù)返回后命锄,其執(zhí)行環(huán)境的作用域鏈會(huì)被銷毀堰乔,但它的活動(dòng)對(duì)象仍然會(huì)留在內(nèi)存中;直到匿名函數(shù)被銷毀,createComparisonFunction()的活動(dòng)對(duì)象才會(huì)被銷毀脐恩。

??例如以下代碼镐侯,返回的匿名函數(shù)被保存在變量compareNames中,通過將compareNames設(shè)置為null來解除對(duì)匿名函數(shù)的引用驶冒,解除引用之后垃圾回收例程將會(huì)清除該匿名函數(shù)苟翻,隨之該匿名函數(shù)的作用域鏈也會(huì)被銷毀,則其作用域鏈上的其他作用域也會(huì)安全的銷毀(全局作用域除外)骗污。

var compareNames = createComparisonFunction("name");
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
compareNames = null;

3.經(jīng)典的閉包實(shí)例

在學(xué)習(xí)閉包的時(shí)候我們很容易見到以下代碼:

for(var i=1; i<=5; i++) {         
    setTimeout( function timer() {
        console.log(i);
    }, i*1000);
}

??如果不理解閉包崇猫,很容易認(rèn)為這段代碼輸出的是1,2,3,4,5,每隔一秒輸出一個(gè)需忿。但是其實(shí)是以每秒一次的頻率輸出56诅炉。因?yàn)檠舆t函數(shù)的回調(diào)會(huì)在循環(huán)執(zhí)行完之后再執(zhí)行(即使setTimeout的第二個(gè)參數(shù)為0,由于事件循環(huán)的機(jī)制屋厘,回調(diào)函數(shù)依然會(huì)在循環(huán)結(jié)束后執(zhí)行)涕烧。

之所以我們錯(cuò)誤的認(rèn)為它會(huì)輸出1~5,是因?yàn)槲覀冏约杭僭O(shè)每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè)i的副本汗洒。但是澈魄,根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中五個(gè)函數(shù)是在各個(gè)迭代中分別定義的仲翎,但是它們都被封閉在同一個(gè)全局作用域中,即實(shí)際上只有一個(gè)i铛漓。

下面我們嘗試將其結(jié)果修改為輸出1~5溯香。那么下邊的代碼行不行呢?

for(var i=1; i<=5; i++) {         
    (function(){
        setTimeout( function timer() {
            console.log(i);
        }, i*1000);
    })();
}

??答案是不行浓恶。確實(shí)玫坛,上邊的代碼在每個(gè)迭代中都添加了一個(gè)獨(dú)有的作用域,但是這個(gè)作用域是空的包晰,沒有對(duì)于i的定義湿镀,所以每次查找i的時(shí)候還是會(huì)向上查找炕吸,找到全局作用域中的i并輸出。在循環(huán)執(zhí)行完之后的i6勉痴,所以依然每次輸出6『漳#現(xiàn)在我們?yōu)槊總€(gè)小的作用域添加一個(gè)變量,其值為循環(huán)時(shí)當(dāng)前的i的值蒸矛。

for(var i=1; i<=5; i++) {         
    (function(i){
        setTimeout( function timer() {
            console.log(i);
        }, i*1000);
    })(i);
}

??現(xiàn)在的結(jié)果已經(jīng)和理想的情況一樣了瀑罗。ES6擁有更簡(jiǎn)潔的方法創(chuàng)建一個(gè)封閉的作用域:let,它會(huì)使一個(gè)塊轉(zhuǎn)換成一個(gè)可以被關(guān)閉的作用域(對(duì)于let定義的變量來說塊就是封閉的)雏掠。所以你可以寫成下邊的代碼:

for(let i=1; i<=5; i++) {         
    setTimeout( function timer() {
        console.log(i);
    }, i*1000);
}

4.利用閉包創(chuàng)建模塊

我們看以下代碼:

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}
var foo = CoolModule();
foo.doSomething();               //cool
foo.doAnother();                 //1 ! 2 ! 3

這個(gè)模式就被稱為模塊斩祭,最常見的實(shí)現(xiàn)模塊模式的方法通常被稱為模塊暴露,這里展示其變體乡话。

模塊模式需要具備兩個(gè)必要條件:
??1.必須有外部的封閉函數(shù)(CoolModule())摧玫,該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)。
??2.封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù)绑青,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包诬像,并且可以訪問或者修改私有的狀態(tài)。


??以上就是我在學(xué)習(xí)閉包時(shí)的一些總結(jié)时迫,歡迎討論颅停。

參考資料:《JavaScript高級(jí)程序設(shè)計(jì)》
?????《你不知道的JavaScript》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掠拳,隨后出現(xiàn)的幾起案子癞揉,更是在濱河造成了極大的恐慌,老刑警劉巖溺欧,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊熟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡姐刁,警方通過查閱死者的電腦和手機(jī)芥牌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來聂使,“玉大人壁拉,你說我怎么就攤上這事“匕校” “怎么了弃理?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)屎蜓。 經(jīng)常有香客問我痘昌,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任辆苔,我火速辦了婚禮算灸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驻啤。我一直安慰自己菲驴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布街佑。 她就那樣靜靜地躺著谢翎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沐旨。 梳的紋絲不亂的頭發(fā)上森逮,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音磁携,去河邊找鬼褒侧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛谊迄,可吹牛的內(nèi)容都是我干的闷供。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼统诺,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼歪脏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起粮呢,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤婿失,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后啄寡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體豪硅,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年挺物,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了懒浮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡识藤,死狀恐怖砚著,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痴昧,我是刑警寧澤赖草,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站剪个,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扣囊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一乎折、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侵歇,春花似錦骂澄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至溃蔫,卻和暖如春健提,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伟叛。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工私痹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人统刮。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓紊遵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親侥蒙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暗膜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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