關(guān)于閉包

本文章著作權(quán)歸饑人谷_Lyndon和饑人谷所有寂嘉,轉(zhuǎn)載請注明出處。

閉包對于我而言是一個難點布近,但閉包又是一個很有用的知識點垫释,很多高級應(yīng)用都需要依賴閉包。
所以在參考一些文章加上大量練習(xí)后撑瞧,我來寫一寫自己理解閉包的過程棵譬,首先是弄清楚以下幾個知識點。


>>> Part 1. 變量的作用域

JS中预伺,變量的作用域只有兩種:全局作用域订咸、函數(shù)作用域。對應(yīng)的變量也只有兩種:全局變量酬诀、局部變量脏嚷。

函數(shù)內(nèi)部可以直接讀取全局變量。

var a = 1;
function f(){
    console.log(a);
}
f();  // 1

但是函數(shù)外部無法讀取到函數(shù)內(nèi)部的局部變量瞒御。

function f(){
    var a = 1;
}
console.log(a);  // Uncaught ReferenceError: a is not defined

這一個Part是比較好理解的父叙。


>>> Part 2. 如何從外部讀取到局部變量?

在祿永老師的公開課中肴裙,老師將從外部讀取局部變量這一情況稱作“偉大的逃脫”趾唱。總結(jié)而言蜻懦,有兩種方法來實現(xiàn)甜癞。

  • 返回值的方法:函數(shù)作為返回值
function f1(){
    var a = 1;
    function f2(){
        console.log(a);
    }
    return f2;
}
var result = f1();
result();  // 1

函數(shù)f2包裹在函數(shù)f1內(nèi),根據(jù)作用域鏈的原理:子對象會一級一級向上尋找父對象的變量宛乃,f1所有的局部變量都可以被f2訪問到悠咱,反之則不行。因此只要把f2作為返回值征炼,就可以在f1外部讀取到其中的內(nèi)部變量析既。

  • 句柄的方法:定義全局變量
var innerHandler = null;
function outerFunc(){
    var outerVar = 1;
    function innerFunc(){
        console.log(outerVar);
        var innerVar = 2;
    }
    innerHandler = innerFunc;
}
outerFunc();
innerHandler();  // 1

這一方法首先定義了一個值為null的全局變量innerHandler,然后讓innerHandler等于函數(shù)內(nèi)部的函數(shù)谆奥,函數(shù)內(nèi)部的函數(shù)則可以通過作用域鏈訪問到父對象的變量outerVar渡贾,之后在外部調(diào)用innerHandler的時候,就可以訪問到outerFunc函數(shù)中的內(nèi)部變量outerVar雄右。


>>> Part 3. 閉包

網(wǎng)絡(luò)上有千萬種對閉包的解釋空骚,其實閉包就是上面例子中的兩個函數(shù):f2以及innerFunc。書面解釋就是:能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)擂仍。

在JS中囤屹,因為父函數(shù)內(nèi)部的子函數(shù)才能夠讀取局部變量,因此閉包的常見形式就是:定義在函數(shù)內(nèi)部的函數(shù)逢渔。前者是后者的充分不必要條件肋坚。

一言以蔽之,閉包就是連接函數(shù)內(nèi)部外部的渠道肃廓。


>>> Part 4. 對示例代碼段的解答

  • 第一段代碼
function outerFn() {
    console.log("Outer function");
    function innerFn() {
        var innerVar = 0;
        innerVar++;
        console.log("Inner function\t");
        console.log("innerVar = "+innerVar+"");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

在這一段代碼當(dāng)中智厌,innerFn不是一個閉包,因為它并不需要讀取其他函數(shù)的內(nèi)部變量盲赊,唯一的變量innerVar就在innerFn函數(shù)內(nèi)部铣鹏。在第一個fnRef()之后,結(jié)果就是首先輸出Outer function哀蘑,然后輸出Inner function诚卸,由于innerVar是函數(shù)innerFn的內(nèi)部變量且自增,因此從0變?yōu)?绘迁,再輸出innerVar = 1.

這時候需要明白合溺,當(dāng)再次運(yùn)行fnRef()時,由于fnRef本身已經(jīng)變成了函數(shù)innerFn缀台,所以其輸出結(jié)果就不再有Outer Function這一句棠赛,而是直接輸出:Inner function以及innerVar = 1.原因是此時的innerVar是一個內(nèi)部變量,其作用域限定在innerFn函數(shù)中膛腐,每次調(diào)用執(zhí)行innerFn函數(shù)睛约,innerVar都會被重寫。

對于下面的fnRef2()依疼,也是同理痰腮。最后的輸出結(jié)果見下圖:

  • 第二段代碼
var globalVar = 0;
function outerFn() {
    console.log("Outer function");
    function innerFn() {
        globalVar++;
        console.log("Inner function\t");
        console.log("globalVar = " + globalVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

這里的globalVar是一個外部變量,也是一個全局變量律罢,處于全局作用域下膀值。所以當(dāng)執(zhí)行innerFn時,innerFn函數(shù)將會訪問到一個每次都自增的全局作用域下的活動對象误辑,因此輸出的結(jié)果會從globalVar = 1一直到globalVar = 4.在執(zhí)行間歇中沧踏,globalVar處于兩個函數(shù)的作用域之外,天高地遠(yuǎn)誰也管不了巾钉,所以它的值會被保存在內(nèi)存中翘狱,并不會立刻被抹去。最后的輸出結(jié)果見下圖:

  • 第三段代碼
function outerFn() {
    var outerVar = 0;
    console.log("Outer function");
    function innerFn() {
        outerVar++;
        console.log("Inner function\t");
        console.log("outerVar = " + outerVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

閉包來臨了砰苍,這里的fnRef是一個閉包innerFn函數(shù)潦匈,但是此時的變量outerVar來到了父函數(shù)的作用域內(nèi)阱高,不像之前一樣處于子函數(shù)作用域內(nèi)或者處于全局作用域下〔缢酰可以發(fā)現(xiàn)赤惊,這和Part 2中的例子非常相似。

其原理是:外部函數(shù)的調(diào)用環(huán)境為相互獨(dú)立的封閉閉包的環(huán)境凰锡,第二次的fnRef2調(diào)用outerFn沒有沿用第一次調(diào)用fnRefouterVar的值未舟,第二次函數(shù)調(diào)用的作用域創(chuàng)建并綁定了一個新的outerVar實例,兩個閉包環(huán)境中的計數(shù)器是相互獨(dú)立掂为,不存在關(guān)聯(lián)的裕膀。

進(jìn)一步來說,在每個封閉閉包環(huán)境中勇哗,外部函數(shù)的局部變量會保存在內(nèi)存中昼扛,并不會在外部函數(shù)調(diào)用后被自動清除。原因在于:outerFninnerFn的父函數(shù)智绸,而innerFn被賦值給一個全局變量野揪,因此innerFn始終在內(nèi)存當(dāng)中,而它又依賴于outerFn瞧栗,所以outerFn也必須始終在內(nèi)存中斯稳,不會再函數(shù)被調(diào)用后就被抹去,因此閉包也有一點點不好迹恐,有可能造成內(nèi)存泄漏挣惰。

所以,結(jié)果應(yīng)該是:outerVar = 1, outerVar = 2, outerVar = 1, outerVar = 2.結(jié)果如下圖所示:

我寫到這自己已經(jīng)完全明白了殴边,我現(xiàn)在要用自己的理解來理順一下最經(jīng)典的問題憎茂。


>>> Part 5. 理順最經(jīng)典問題

<div id="divTest">
    <span>0</span>
    <span>1</span>
    <span>2</span>
    <span>3</span>
</div>
<script>
    var spans = document.querySelectorAll("#divTest span");
    for(var i = 0; i < spans.length; i++){
        spans[i].onclick = function(){
            console.log(i);
        }
    }
</script>

最經(jīng)典的問題是:為什么我點擊任何數(shù)字,控制臺的輸出結(jié)果永遠(yuǎn)是4锤岸?

這里可使用作用域鏈來幫助理解竖幔,不妨將以上代碼轉(zhuǎn)化為:

// function只是傳遞給了NodeList類型對象中的元素卻并未執(zhí)行,因為后面無括號
spans[0] = function fn0(){console.log(i)};
spans[1] = function fn1(){console.log(i)};
spans[2] = function fn2(){console.log(i)};
spans[3] = function fn3(){console.log(i)};
globalContext = {
    AO: {
        i: undefined, // 0(fn0)1(fn1)2(fn2)3(fn3)4(終止循環(huán))
        spans:[0], [1], [2], [3]
    },
    scope: null
}
fn0[[scope]] = globalContext.AO,
fn1[[scope]] = globalContext.AO,
fn2[[scope]] = globalContext.AO,
fn3[[scope]] = globalContext.AO

fn0Context = {
    AO:{
    },
    scope: fn0[[scope]]
}

fn1Context = {
    AO:{
    },
    scope: fn1[[scope]]
}

fn2Context = {
    AO:{
    },
    scope: fn2[[scope]]
}

fn3Context = {
    AO:{
    },
    scope: fn3[[scope]]
}

最后點擊span元素的時候i早已變?yōu)?是偷,因此永遠(yuǎn)輸出4.

改進(jìn)的方法可以使用閉包拳氢,也就是:

var spans = document.querySelectorAll("#divTest span");
for(var i = 0; i < spans.length; i++) {
    spans[i].onclick = function(i){
        return function (){
            console.log(i);
        }
    }(i);
}

這個閉包也可以用作用域鏈來理解:

globalContext = {
    AO:{
        i: undefined,
        spans: [0], [1], [2], [3]
    }
}
fn0.scope = globalContext.AO,
fn1.scope = globalContext.AO,
fn2.scope = globalContext.AO,
fn3.scope = globalContext.AO

fn0Context = {
    AO:{
        i: 0,
        function: anonymous
    }
    fn0[[scope]] = fn0.scope // globalContext.AO 
}

function_anonymousContext = {
    AO: {
    }
    function_anonymous[[scope]] = fn0Context.AO
}
...

>>> Part 6. 閉包的問題

如同剛才的分析一樣,當(dāng)涉及到閉包時蛋铆,函數(shù)中的變量都會被保存在內(nèi)存中馋评,因此需要避免濫用閉包,否則就有可能導(dǎo)致內(nèi)存泄露刺啦。


>>> 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末留特,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜕青,老刑警劉巖苟蹈,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異右核,居然都是意外死亡汉操,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門蒙兰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人芒篷,你說我怎么就攤上這事搜变。” “怎么了针炉?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵挠他,是天一觀的道長。 經(jīng)常有香客問我篡帕,道長殖侵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任镰烧,我火速辦了婚禮拢军,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怔鳖。我一直安慰自己茉唉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布结执。 她就那樣靜靜地躺著度陆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪献幔。 梳的紋絲不亂的頭發(fā)上懂傀,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音蜡感,去河邊找鬼蹬蚁。 笑死,一個胖子當(dāng)著我的面吹牛铸敏,可吹牛的內(nèi)容都是我干的缚忧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼杈笔,長吁一口氣:“原來是場噩夢啊……” “哼闪水!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤球榆,失蹤者是張志新(化名)和其女友劉穎朽肥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體持钉,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衡招,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了每强。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片始腾。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖空执,靈堂內(nèi)的尸體忽然破棺而出浪箭,到底是詐尸還是另有隱情,我是刑警寧澤辨绊,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布奶栖,位于F島的核電站,受9級特大地震影響门坷,放射性物質(zhì)發(fā)生泄漏宣鄙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一默蚌、第九天 我趴在偏房一處隱蔽的房頂上張望冻晤。 院中可真熱鬧,春花似錦敏簿、人聲如沸明也。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温数。三九已至,卻和暖如春蜻势,著一層夾襖步出監(jiān)牢的瞬間撑刺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工握玛, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留够傍,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓挠铲,卻偏偏與公主長得像冕屯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拂苹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

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