本文首發(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開始向后查找:
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ù)的作用域鏈如下圖所示:
??在匿名函數(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í)是以每秒一次的頻率輸出5
次6
诅炉。因?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í)行完之后的i
是6
勉痴,所以依然每次輸出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》