從一個(gè)函數(shù)的實(shí)現(xiàn)說起
話說秸妥,我要寫這么一個(gè)函數(shù) getCallCounter
, 該函數(shù)無參數(shù),返回一個(gè)數(shù)字沃粗,第一次調(diào)用返回1粥惧,第二次調(diào)用返回2,... 最盅,以此類推突雪,即
console.log( getCallCounter() ); //輸出為1
console.log( getCallCounter() ); //輸出為2
console.log( getCallCounter() ); //輸出為3
全局變量實(shí)現(xiàn)
拿到這個(gè)需求之后起惕,我們最簡單的想法就是寫一個(gè)全局變量,并初始化為0咏删,每次調(diào)用 getCallCounter
時(shí)惹想,首先將該全局變量加1,并返回該全局變量督函,代碼如下:
var g_counter = 0;
function getCallCounter(){
g_counter += 1;
return g_counter;
}
上述代碼非常簡單嘀粱,也達(dá)到了要求。但是有一個(gè)問題辰狡,我們定義了一個(gè) “累贅” -- g_counter
锋叨,g_counter
的定義導(dǎo)致了2個(gè)問題:
污染了全局作用域區(qū),因?yàn)橹挥泻瘮?shù)
getCallCounter
會(huì)用到該變量宛篇,所以娃磺,該變量應(yīng)該遵守 "誰要使用,誰來管理" 的原則叫倍,而不是放在全局變量區(qū)偷卧;任何代碼段都能取得
g_counter
,導(dǎo)致了getCallCounter
對(duì)g_counter
不能做到完全控制段标。如果其他代碼修改了g_counter
涯冠,那就不能保證每次調(diào)用getCallCounter
,我們都能得到一個(gè)遞增的數(shù)逼庞;
以上兩點(diǎn)不足是相互聯(lián)系的,正是由于明明應(yīng)該限定在 getCallCounter
函數(shù)中的變量卻放在全局區(qū)瞻赶,才導(dǎo)致上面兩個(gè)問題赛糟。
因此,我們將定義放到函數(shù)中砸逊;
改進(jìn)1
function getCallCounter(){
var counter = 0;
counter += 1;
return counter;
}
改成上述代碼后璧南, counter
受到 getCallCounter
的完全控制,但是师逸,每次調(diào)用該函數(shù)后司倚,返回都是1;
這是因?yàn)楹瘮?shù)有自己的作用域篓像,每次完成函數(shù)調(diào)用动知,函數(shù)中的變量不再被引用,之后將會(huì)被 垃圾回收機(jī)制 所銷毀员辩,再次調(diào)用時(shí)盒粮,又會(huì)創(chuàng)建新的局部變量,因此奠滑, getCallCounter
返回的一直是 1丹皱;
C語言的實(shí)現(xiàn)
為了讓延長函數(shù)中局部變量的聲明周期(而不是函數(shù)執(zhí)行完后妒穴,內(nèi)存就被釋放掉),C語言中通過關(guān)鍵字 static
為其 "增壽"摊崭;
int getCallCounter(){
static int counter = 0;
counter += 1;
return counter;
}
這樣讼油,我們每次調(diào)用 getCallCounter
,都能夠得到一個(gè)遞增的數(shù)呢簸;
但這畢竟是C語言的實(shí)現(xiàn)矮台,那么JavaScript如何實(shí)現(xiàn)呢?
解決生命周期問題
我們首先要做的阔墩,就是像C語言一樣解決局部變量生命周期過短的問題嘿架。
為了保證不被 垃圾回收機(jī)制 這個(gè) "劊子手" 所干掉,我們必須始終保持對(duì)局部變量的使用啸箫,因?yàn)?垃圾回收機(jī)制 只是針對(duì)沒用的數(shù)據(jù)耸彪,而那些還會(huì)被使用的數(shù)據(jù)是不會(huì)被當(dāng)成垃圾的!
所以我們有了下面的代碼:
function f(){
var counter = 0;
return function(){
counter += 1;
return counter;
}
}
var getCallCounter = f();
首先忘苛,我們定義了一個(gè)函數(shù) f 蝉娜,該函數(shù)返回一個(gè)函數(shù),因?yàn)榉祷氐暮瘮?shù)能夠獲得 f 中的局部變量扎唾,所以返回的函數(shù)能夠得到變量 counter
的值召川,即 在全局變量作用域中調(diào)用一個(gè)函數(shù)得到了局部作用域中的變量。
其次胸遇,我們通過變量 getCallCounter
來存放返回的函數(shù)荧呐,說明返回的函數(shù)始終存在變量對(duì)該函數(shù)引用,因此該返回函數(shù)不會(huì)被銷毀纸镊;而返回的函數(shù)又使用了局部變量 counter
倍阐,那么解釋器就認(rèn)為 counter
也是有用的,只要 getCallCounter
不被銷毀逗威,該臨時(shí)變量也將不會(huì)被銷毀峰搪;
閉包的官方定義是:
一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù)),因而這些變量也是該表達(dá)式的一部分凯旭。
Excuse me ? 官方的定義總是會(huì)用一些看起來高大上的話讓你覺得牛逼概耻,但是,你就是看不懂罐呼。鞠柄。。
我的理解是:就是一個(gè)函數(shù)的返回值是一個(gè)函數(shù)弄贿,那么就形成一個(gè)函數(shù)被另一個(gè)函數(shù)所 封閉 的情形春锋,而被返回的函數(shù)能夠調(diào)用定義它的函數(shù)中的所有變量,這就形成閉包差凹;
雖然可能不準(zhǔn)確期奔,但至少好記憶侧馅,好理解。
美化
我們看到前面的函數(shù) f 只被調(diào)用了一次呐萌,對(duì)于這些只調(diào)用一次的函數(shù)馁痴,完全可以寫成 匿名的立即執(zhí)行的 函數(shù),修改后肺孤,代碼為:
var getCallCounter = ( function(){
var counter = 0;
return function(){
counter += 1;
return counter;
}
} )();
如此罗晕,便可優(yōu)雅的完成最開始的要求。
完赠堵。