原文地址:深入理解閉包(六)——閉包
終于講到閉包了,這一路走來(lái)不容易。
從前面的博文中我們知道,js的垃圾回收機(jī)制會(huì)在某個(gè)函數(shù)的執(zhí)行上下文生命周期結(jié)束后將其回收,釋放內(nèi)存,但是閉包的存在會(huì)阻止這一過(guò)程。
概念
js高程對(duì)閉包的定義是:閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。 創(chuàng)建閉包的常見方式就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù),作為返回值或參數(shù)傳遞到函數(shù)外部。但是根據(jù)我的經(jīng)驗(yàn),只要在一個(gè)函數(shù)內(nèi)部使用了關(guān)鍵字function,一個(gè)閉包就被創(chuàng)建了。
實(shí)例
任何書面上的解釋都不如一個(gè)實(shí)例來(lái)的有效。
var a = 1;
function test() {
var b = 2;
return function () {
var c = 3;
console.log(a+b+c);
}
}
var result=test();
result(); //6
按照我們之前的說(shuō)法,函數(shù)外部是不能訪問(wèn)函數(shù)內(nèi)部的變量的,代碼執(zhí)行到var result=test()這句時(shí)調(diào)用test函數(shù),執(zhí)行完畢后應(yīng)該銷毀執(zhí)行上下文,其中的變量都不能再訪問(wèn),但是執(zhí)行result()時(shí)卻仍然調(diào)用了test中的變量,這是因?yàn)槲覀冊(cè)趖est函數(shù)中返回了一個(gè)匿名函數(shù)。
test函數(shù)中返回的不僅僅是一個(gè)函數(shù),還有它的執(zhí)行上下文環(huán)境,將其賦值給全局變量result后,那么result其實(shí)就等同于那個(gè)返回的匿名函數(shù),當(dāng)它被調(diào)用時(shí)可以訪問(wèn)匿名函數(shù)作用域鏈中指向的變量對(duì)象(并不是把變量對(duì)象復(fù)制給了result,只是可以被引用),這個(gè)返回的匿名函數(shù)就是閉包。
引用阮一峰老師對(duì)閉包的解釋:閉包本質(zhì)上是將函數(shù)內(nèi)部和外部連接起來(lái)的橋梁。
再看一個(gè)栗子:
function out() {
var a = 1;
var inner=function () {
console.log(a);
}
a++;
return inner;
}
var b = out();
b(); //2
肯定會(huì)有人覺得輸出的應(yīng)該是1,想著a++在console.log(a)后面,這是一種常見的錯(cuò)覺,代碼創(chuàng)建的位置和執(zhí)行的順序是不一樣的,這點(diǎn)要謹(jǐn)記。這段代碼里首先執(zhí)行的是調(diào)用out函數(shù),out函數(shù)執(zhí)行代碼,最后一步返回inner,要知道a++是在return之前執(zhí)行的,a=2已經(jīng)保存到了變量對(duì)象中,接下來(lái)調(diào)用b函數(shù)(等同于調(diào)用inner函數(shù)),輸出的a自然就是2.
復(fù)雜一點(diǎn)的栗子:
var fun1,fun2,fun3;
function test() {
var a=10;
fun1=function () {console.log(a);};
fun2=function () {a++;};
fun3=function (x){a=x;};
}
test();
fun2();
fun1(); //11
fun3(5);
fun1(); //5
var fun4=fun1;
test();
fun1(); //10
fun4(); //5
如果上一個(gè)栗子你已經(jīng)理解,那么理解這個(gè)栗子前兩次輸出的11和5也不是什么難事,但后面的兩個(gè)輸出10和5你可能會(huì)有點(diǎn)疑惑。
- 我們第二次調(diào)用test函數(shù)時(shí)一個(gè)新的閉包被創(chuàng)建,它能訪問(wèn)到的變量也是重新創(chuàng)建的,跟前面的沒(méi)有關(guān)系,因此再調(diào)用fun1時(shí)輸出10。我們要記住這句話:如果你在一個(gè)函數(shù)內(nèi)部聲明了另一個(gè)函數(shù),那么這個(gè)外部函數(shù)每次被調(diào)用都會(huì)產(chǎn)生一個(gè)閉包,創(chuàng)建嶄新的執(zhí)行上下文環(huán)境。
- 那么調(diào)用fun4怎么又輸出了5呢,這是因?yàn)関ar fun4=fun1是在第一次調(diào)用test發(fā)生的,那么fun4可以訪問(wèn)的變量也是第一次調(diào)用test時(shí)創(chuàng)建的變量對(duì)象,即使在別的地方被調(diào)用,它的作用域鏈也就是可訪問(wèn)的變量是不變的。我們要記住這句話:一個(gè)函數(shù)可以訪問(wèn)的變量對(duì)象要到創(chuàng)建這個(gè)函數(shù)的執(zhí)行環(huán)境中去找而不是調(diào)用這個(gè)函數(shù)的執(zhí)行環(huán)境。
還有一個(gè)非常常見的栗子,那就是閉包對(duì)循環(huán)的影響:
var arr=[];
function test(){
for (i= 0;i<3;i=i+1){
arr[i]=function(){
console.log(i);
};
}
}
test();
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3
函數(shù)運(yùn)行之后會(huì)得到一個(gè)函數(shù)數(shù)組,你本想讓每個(gè)函數(shù)都返回自己的索引值,比如運(yùn)行arr[0]()時(shí)得到0,運(yùn)行arr[1]()時(shí)得到1。但實(shí)際上,每個(gè)函數(shù)都輸出3。這是因?yàn)?strong>閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值,每個(gè)函數(shù)的作用域鏈中都保存著test函數(shù)中的變量對(duì)象,所以它們引用的是同一個(gè)變量i,而i的最后一個(gè)值是3,因此每個(gè)函數(shù)都輸出3。我們可以通過(guò)創(chuàng)建一個(gè)自執(zhí)行匿名函數(shù)來(lái)讓閉包符合預(yù)期:
var arr=[];
function test(){
for (i= 0;i<3;i=i+1){
arr[i]=(function(num){
console.log(num);
})(i);
}
}
test(); //0,1,2
此處用到了自執(zhí)行匿名函數(shù)(function(){···})(),它的寫法是,在函數(shù)體外面加一對(duì)圓括號(hào),形成一個(gè)表達(dá)式,在圓括號(hào)后面再加一個(gè)圓括號(hào),里面可傳入?yún)?shù)。由于函數(shù)參數(shù)是按值傳遞的,所以就可以把變量i的當(dāng)前值賦值給匿名函數(shù)的參數(shù)num,而自執(zhí)行匿名函數(shù)可以不用調(diào)用,自己執(zhí)行自己,因此只要檢測(cè)到參數(shù)i就會(huì)立即執(zhí)行并把結(jié)果傳遞給arr數(shù)組。這樣一來(lái)我們就能得到預(yù)期的結(jié)果了。
缺點(diǎn)
最后一個(gè)栗子告訴我們,閉包有時(shí)也會(huì)給我們的工作帶來(lái)負(fù)面效果。除此之外,還有兩個(gè)缺點(diǎn)我們需要注意一下。
- 內(nèi)存泄漏: 在ie9之前的版本中,如果閉包的作用域鏈中保存著一個(gè)html元素,那么就意味著該元素將無(wú)法銷毀。
- 占用內(nèi)存:由于閉包會(huì)攜帶包含它的函數(shù)的作用域,因此會(huì)比其他函數(shù)占用更多的內(nèi)存。過(guò)度使用閉包可能會(huì)導(dǎo)致內(nèi)存占用過(guò)多,所以我們最好只在絕對(duì)必要時(shí)再考慮使用閉包。