一素邪、變量的作用域
要懂得閉包妻顶,起首必須懂得Javascript特別的變量作用域捆等。
變量的作用域無非就是兩種:全局變量和局部變量氓皱。
Javascript說話的特別之處路召,就在于函數(shù)內(nèi)部可以直接讀取全局變量勃刨。
Js代碼
···
var n=999;
function f1(){
alert(n);
}
f1(); // 999
···
另一方面,在函數(shù)外部天然無法讀取函數(shù)內(nèi)的局部變量股淡。
Js代碼
function f1(){
var n=999;
}
alert(n); // error
這里有一個(gè)處所須要重視身隐,函數(shù)內(nèi)部聲明變量的時(shí)候,必然要用var唯灵。若是不用的話贾铝,你實(shí)際上聲明了一個(gè)全局變量!
Js代碼
function f1(){
n=999;
}
f1();
alert(n); // 999
--------------------------------------------------------------------------------------------------------
二埠帕、如何從外部讀取局部變量垢揩?
出于各種原因,我們有時(shí)要獲得函數(shù)內(nèi)的局部變量。然則,前面已經(jīng)說過了昌罩,正常情況下,這是辦不到的俘种,只有經(jīng)由過程變通才能實(shí)現(xiàn)。
那就是在函數(shù)的內(nèi)部绝淡,再定義一個(gè)函數(shù)宙刘。
Js代碼
function f1(){
n=999;
function f2(){
alert(n); // 999
}
}
在上方的代碼中,函數(shù)f2就被包含在函數(shù)f1內(nèi)部牢酵,這時(shí)f1內(nèi)部的所有局部變量悬包,對(duì)f2都是可見的。然則反過來就不可馍乙,f2內(nèi)部的局部變量布近,對(duì)f1 就是不成見的。這就是Javascript說話特有的“鏈?zhǔn)阶饔糜颉辈季郑╟hain scope)丝格,
子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量撑瞧。所以,父對(duì)象的所有變量显蝌,對(duì)子對(duì)象都是可見的预伺,反之則不成立。
既然f2可以讀取f1中的局部變量曼尊,那么只要把f2作為返回值酬诀,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
Js代碼
function f1(){
n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
--------------------------------------------------------------------------------------------------------
三骆撇、閉包的概念
上一節(jié)代碼中的f2函數(shù)瞒御,就是閉包。
各類專業(yè)文獻(xiàn)上的“閉包”(closure)定義很是抽象艾船,很丟臉懂葵腹。我的懂得是高每,閉包就是可以或許讀取其他函數(shù)內(nèi)部變量的函數(shù)屿岂。
因?yàn)樵贘avascript說話中践宴,只有函數(shù)內(nèi)部的子函數(shù)才干讀取局部變量,是以可以把閉包簡(jiǎn)單懂得成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”爷怀。
所以阻肩,在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁运授。
--------------------------------------------------------------------------------------------------------b
四烤惊、閉包的用處
閉包可以用在很多處所。它的最大用處有兩個(gè)吁朦,一個(gè)是前面提到的可以讀取函數(shù)內(nèi)部的變量柒室,另一個(gè)就是讓這些變量的值始終對(duì)峙在內(nèi)存中。
怎么來懂得這句話呢逗宜?請(qǐng)看下面的代碼雄右。
Js代碼
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在這段代碼中,result實(shí)際上就是閉包f2函數(shù)纺讲。它一共運(yùn)行了兩次擂仍,第一次的值是999,第二次的值是1000熬甚。這就說明逢渔,函數(shù)f1中的局部變量n一向保存在內(nèi)存中,并沒有在f1調(diào)用后被主動(dòng)清除乡括。
為什么會(huì)這樣呢肃廓?原因就在于f1是f2的父函數(shù),而f2被賦給了一個(gè)全局變量诲泌,這導(dǎo)致f2始終在內(nèi)存中盲赊,而f2的存在依附于f1,是以f1也始終在內(nèi)存中档礁,不會(huì)在調(diào)用停止后角钩,被垃圾收受接管機(jī)制(garbage collection)收受接管。
這段代碼中另一個(gè)值得重視的一處呻澜,就是“nAdd=function(){n+=1}”這一行递礼,起首在nAdd前面沒有應(yīng)用var關(guān)鍵字,是以 nAdd是一個(gè)全局變量羹幸,而不是局部變量脊髓。其次,nAdd的值是一個(gè)匿名函數(shù)(anonymous function)栅受,而這個(gè)
匿名函數(shù)本身也是一個(gè)閉包将硝,所以nAdd相當(dāng)于是一個(gè)setter恭朗,可以在函數(shù)外部對(duì)函數(shù)內(nèi)部的局部變量進(jìn)行操縱。
--------------------------------------------------------------------------------------------------------
五依疼、應(yīng)用閉包的重視點(diǎn)
1)因?yàn)殚]包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中痰腮,內(nèi)存消費(fèi)很大,所以不要濫用閉包律罢,不然會(huì)造成網(wǎng)頁的性能題目膀值,在IE中可能導(dǎo)致內(nèi)存泄漏。解決辦法是误辑,在退出函數(shù)之前沧踏,將不應(yīng)用的局部變量全部刪除。
2)閉包會(huì)在父函數(shù)外部巾钉,改變父函數(shù)內(nèi)部變量的值翘狱。所以,若是你把父函數(shù)當(dāng)做難象(object)應(yīng)用砰苍,把閉包算作它的公用辦法(Public Method)潦匈,把內(nèi)部變量算作它的私有屬性(private value),這時(shí)必然要警惕师骗,不要隨便
改變父函數(shù)內(nèi)部變量的值历等。
有權(quán)訪問另一個(gè)函數(shù)作用域內(nèi)變量的函數(shù)都是閉包。
什么是閉包辟癌?
先看一段代碼:
function a(){
var n = 0;
function inc() {
n++;
console.log(n);
}
inc();
inc();
}
a(); //控制臺(tái)輸出1寒屯,再輸出2
簡(jiǎn)單吧。再來看一段代碼:
function a(){
var n = 0;
this.inc = function () {
n++;
console.log(n);
};
}
var c = new a();
c.inc(); //控制臺(tái)輸出1
c.inc(); //控制臺(tái)輸出2
簡(jiǎn)單吧黍少。
什么是閉包寡夹?這就是閉包!
有權(quán)訪問另一個(gè)函數(shù)作用域內(nèi)變量的函數(shù)都是閉包厂置。
這里 inc 函數(shù)訪問了構(gòu)造函數(shù) a 里面的變量 n菩掏,所以形成了一個(gè)閉包。
再來看一段代碼:
function a(){
var n = 0;
function inc(){
n++;
console.log(n);
}
return inc;
}
var c = a();
c(); //控制臺(tái)輸出1
c(); //控制臺(tái)輸出2
看看是怎么執(zhí)行的:
var c = couter()昵济,這一句 couter()返回的是函數(shù) inc智绸,那這句等同于 var c = inc;
c(),這一句等同于 inc(); 注意访忿,函數(shù)名只是一個(gè)標(biāo)識(shí)(指向函數(shù)的指針)瞧栗,而()才是執(zhí)行函數(shù)。
后面三句翻譯過來就是: var c = inc; inc(); inc();海铆,跟第一段代碼有區(qū)別嗎迹恐? 沒有。
什么是閉包卧斟?這就是閉包殴边!
所有的教科書教程上都喜歡用最后一段來說明閉包憎茂,但我覺得這將問題復(fù)雜化了。這里面返回的是函數(shù)名锤岸,沒看過譚浩強(qiáng)C/C++程序設(shè)計(jì)的同學(xué)可能一下子沒反應(yīng)出帶不帶()的區(qū)別竖幔,也就是說這種寫法自帶一個(gè)陷阱。雖然這種寫法更顯高大上能耻,但我還是喜歡將問題單一化赏枚,看看代碼 1 和代碼 2亡驰,你還會(huì)糾結(jié)函數(shù)的調(diào)用晓猛,你會(huì)糾結(jié) n 的值嗎?
為啥要這樣寫凡辱?
我們知道戒职,js的每個(gè)函數(shù)都是一個(gè)個(gè)小黑屋,它可以獲取外界信息透乾,但是外界卻無法直接看到里面的內(nèi)容洪燥。將變量 n 放進(jìn)小黑屋里,除了 inc 函數(shù)之外乳乌,沒有其他辦法能接觸到變量 n捧韵,而且在函數(shù) a 外定義同名的變量 n 也是互不影響的,這就是所謂的增強(qiáng)“封裝性”汉操。
而之所以要用 return 返回函數(shù)標(biāo)識(shí) inc再来,是因?yàn)樵?a 函數(shù)外部無法直接調(diào)用 inc 函數(shù),所以 return inc 與外部聯(lián)系起來磷瘤,代碼 2 中的 this 也是將 inc 與外部聯(lián)系起來而已芒篷。
常見的陷阱
看看這個(gè):
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
console.log(funcsi);
}
乍一看,以為輸出 0~9 采缚,萬萬沒想到輸出10個(gè)10针炉?
這里的陷阱就是:函數(shù)帶()才是執(zhí)行函數(shù)! 單純的一句 var f = function() { alert(‘Hi’); }; 是不會(huì)彈窗的扳抽,后面接一句 f(); 才會(huì)執(zhí)行函數(shù)內(nèi)部的代碼篡帕。上面代碼翻譯一下就是:
var result = new Array(), i;
result[0] = function(){ return i; }; //沒執(zhí)行函數(shù),函數(shù)內(nèi)部不變贸呢,不能將函數(shù)內(nèi)的i替換镰烧!
result[1] = function(){ return i; }; //沒執(zhí)行函數(shù),函數(shù)內(nèi)部不變贮尉,不能將函數(shù)內(nèi)的i替換拌滋!
...
result[9] = function(){ return i; }; //沒執(zhí)行函數(shù),函數(shù)內(nèi)部不變猜谚,不能將函數(shù)內(nèi)的i替換败砂!
i = 10;
funcs = result;
result = null;
console.log(i); // funcs0就是執(zhí)行 return i 語句赌渣,就是返回10
console.log(i); // funcs1就是執(zhí)行 return i 語句,就是返回10
...
console.log(i); // funcs9就是執(zhí)行 return i 語句昌犹,就是返回10
為什么只垃圾回收了 result坚芜,但卻不收了 i 呢? 因?yàn)?i 還在被 function 引用著啊斜姥。好比一個(gè)餐廳鸿竖,盤子總是有限的,所以服務(wù)員會(huì)去巡臺(tái)回收空盤子铸敏,但還裝著菜的盤子他怎么敢收缚忧? 當(dāng)然,你自己手動(dòng)倒掉了盤子里面的菜(=null)杈笔,那盤子就會(huì)被收走了闪水,這就是所謂的內(nèi)存回收機(jī)制。
至于 i 的值怎么還能保留蒙具,其實(shí)從文章開頭一路讀下來球榆,這應(yīng)該沒有什么可以糾結(jié)的地方。盤子里面的菜禁筏,吃了一塊不就應(yīng)該少一塊嗎持钉?
總結(jié)一下
閉包就是一個(gè)函數(shù)引用另外一個(gè)函數(shù)的變量,因?yàn)樽兞勘灰弥圆粫?huì)被回收篱昔,因此可以用來封裝一個(gè)私有變量每强。這是優(yōu)點(diǎn)也是缺點(diǎn),不必要的閉包只會(huì)徒增內(nèi)存消耗旱爆!另外使用閉包也要注意變量的值是否符合你的要求舀射,因?yàn)樗拖褚粋€(gè)靜態(tài)私有變量一樣。閉包通常會(huì)跟很多東西混搭起來怀伦,接觸多了才能加深理解脆烟,這里只是開個(gè)頭說說基礎(chǔ)性的東西。