淺析 JavaScript 中的閉包(Closures)

一、前言

對于 JavaScript 來說,閉包是一個非常強大的特征。但對于剛開始接觸的初學(xué)者來說它又似乎是特別高深的。今天我們一起來揭開閉包的神秘面紗。閉包這一塊也有很多的文章介紹過了碉考,今天我就淺談一下自己對閉包的的一些理解,希望能提供一點鄙陋的見解幫助到正在學(xué)習(xí)的朋友挺身。該文章中能使用口語化的我將盡量使用口語化的敘述方式侯谁,希望能讓讀者更好理解,畢竟文章寫出來宗旨就是要讓人讀懂。文章難免有不足之處還希望幫忙指出良蒸。

二技扼、Javascript 的作用域鏈

在了解閉包之前,我們先來看看幾個準(zhǔn)備知識嫩痰。

1.變量的作用域

首先剿吻,什么是作用域?域串纺,區(qū)域丽旅。簡單來理解就是一個變量能被訪問的范圍(區(qū)域)。換言之就是這個變量能起作用的區(qū)域纺棺。按這個標(biāo)準(zhǔn)來劃分我們將變量分為 全局變量局部變量 兩種

以定義的方式來區(qū)分有以下特點:

定義在函數(shù)內(nèi)部的變量是局部變量榄笙,定義在函數(shù)外部的變量是全局變量。(這個并不只是 Javascript 語言的特點)局部變量在函數(shù)內(nèi)部能被訪問祷蝌,在函數(shù)外部不能被直接訪問茅撞,所以局部變量就是從定義它的地方開始到函數(shù)結(jié)束的位置結(jié)束。當(dāng)然這里有個細(xì)節(jié)--變量聲明提升巨朦。等下我們用一小段代碼提一下變量聲明提升是什么米丘。我們先來看看局部變量和全局變量的代碼

    var a = 0;

    function testFunc(){
        var b = 1;
        console.log('-------------函數(shù)內(nèi)輸出-------------');
        console.log(a);//0
        console.log(b);//1
    }
    
    //調(diào)用函數(shù)
    testFunc();

    console.log('-------------函數(shù)外輸出-------------');
    console.log(a);//0
    console.log(b);//Uncaught ReferenceError: b is not defined

執(zhí)行以上代碼結(jié)果如下圖所示

代碼執(zhí)行結(jié)果

在代碼的最后一行拋出了 b 未定義的異常.也就是說我們在函數(shù)外部訪問不到在函數(shù)內(nèi)部定義的局部變量。但是第六行代碼的正常輸出,可見在函數(shù)內(nèi)部我們是可以訪問到在函數(shù)外部定義的全局變量 a

變量聲明提升

相信如果學(xué)過 C 語言的話,應(yīng)該會很熟悉一句話 "先聲明后使用"糊啡。就是說一個變量或者函數(shù)在使用它之前必須是要先找得到這個變量或函數(shù)的聲明的拄查。例如:

    //C 語言正確寫法
    int a = 0;
    printf(a);

    //錯誤寫法,下面代碼沒辦法通過標(biāo)準(zhǔn)編譯(直接報異常)
    printf(a);
    int a = 0;

我們再來看看 Javascript 代碼

    var a = 0;
    console.log(a);//輸出結(jié)果 0

上面這種普通寫法我們不探討,重點看下面的這段代碼

    console.log(a);//輸出結(jié)果 undefined
    var a = "hello";
    console.log(a);//輸出結(jié)果 hello

運行結(jié)果如下

變量聲明提升

上面這個例子就恰好說明了變量聲明提升的特點棚蓄,我們在沒有聲明變量 a 之前就直接訪問變量a 輸出結(jié)果為 undefined 而并不是直接報異常堕扶。所以最直觀的感覺是變量的聲明被提升到使用之前了。實質(zhì)上代碼如下:

    var a;//聲明被提升到這里
    console.log(a);//輸出結(jié)果 undefined
    a = "hello";
    console.log(a);//輸出結(jié)果 hello

小結(jié)一下

  • 函數(shù)內(nèi)部定義的變量是局部變量梭依,函數(shù)外部定義的變量是全局變量稍算。
  • 局部變量不能被外界直接訪問,全局變量可以在函數(shù)內(nèi)被訪問役拴。
  • 變量聲明提升

2.嵌套函數(shù)的作用域特點

搞清楚上面的小結(jié)部分我們縷一縷思路繼續(xù)探討另一個話題糊探,javascript 中的嵌套函數(shù),我們先上一段代碼:

    function A(param){
        var vara = 1;    
        function B(){
            var varb = 2;
            console.log("----Function B----------")
            console.log(vara);//函數(shù)B中訪問A函數(shù)中定義的變量
            console.log(param);//A函數(shù)中傳進來的變量
            console.log(varb);//訪問自身函數(shù)內(nèi)定義的變量
        }
        B();
        console.log("----Function A----------")
        console.log(vara);//訪問自身函數(shù)內(nèi)定義的變量
        console.log(param);//A函數(shù)中傳進來的變量
        console.log(varb);//訪問B函數(shù)中定義的變量--異常
    }
    A("hello");

運行結(jié)果如下:

函數(shù)嵌套

由此可見嵌套函數(shù)(B)可以繼承容器函數(shù)(A)的參數(shù)和變量,但是嵌套函數(shù)(B)中的變量對于他的容器函數(shù)來說卻是B私有的扎狱,也就是說 A 無法訪問 B 中定義的變量侧到。換句話說勃教,B 函數(shù)形成了一個相對獨立的環(huán)境(空間)使得它自身的變量只能由它自己來訪問淤击,但是 A 函數(shù)里的變量 B 也可以訪問,這里嵌套函數(shù) B 就形成了一個閉包故源。有一句話很適合 B 來說 “你的就是我的污抬,我的還是我的”

從語法上看是函數(shù) A 包含著函數(shù) B,但是從作用域上來看是函數(shù) B 的作用域包含著函數(shù) A 的作用域,關(guān)系如下圖所示:


函數(shù)嵌套

假設(shè):函數(shù) B 下面又包含了函數(shù) C印机。此時函數(shù) C 為函數(shù) B 的嵌套函數(shù)矢腻,函數(shù) B 為函數(shù) C 的容器函數(shù)。對于C來說也具有剛剛講過的 “你的就是我的射赛,我的還是我的” 的特點多柑。以此類推層層嵌套的話就形成了一條鏈條, 作用域按此規(guī)律也形成了 Javascript 中的作用域鏈楣责。

函數(shù)嵌套

三竣灌、閉包的特點

我們先來總結(jié)上面提到的兩點

  • 嵌套在容器函數(shù)(A)內(nèi)部的嵌套函數(shù)(B)只能在容器函數(shù)(A)內(nèi)被訪問
  • 嵌套函數(shù)(B)繼承了容器函數(shù)(A)的變量,但是 B 函數(shù)中的變量只有它自己能訪問秆麸,也就是嵌套函數(shù)(B)的作用域包含容器函數(shù)(A)的作用域初嘹。
閉包之保存變量

我們還是先上一段代碼

function A(a){
    function B(b){
        return a + b;
    }
    return B;
}
var C = A(1);
var result = C(2);
console.log(result);//輸出結(jié)果 3 

函數(shù) B 形成了一個閉包,A 函數(shù)調(diào)用之后返回函數(shù) B 的引用沮趣。執(zhí)行 C 之后發(fā)現(xiàn)結(jié)果等于3屯烦,這也就說明了我們調(diào)用 A 的時候 傳進去的參數(shù) 1 沒有被銷毀,而是被保存起來了房铭,這就是閉包保存變量的特點驻龟。

有保存就有銷毀那我們被閉包保存的變量在什么時候銷毀?答案是當(dāng) B 沒有再被引用的時候,就會被銷毀.

閉包的注意點--命名沖突

我們還是先上一段代碼

function A(){
    var num = 6;//外部的名為num 的變量
    function B(num){
        return num;//當(dāng)做參數(shù)傳進來的num 變量,命名沖突發(fā)生在這
    }
    return B;
}
var result = A()(10);
console.log(result);//輸出結(jié)果10

上述代碼的執(zhí)行結(jié)果

閉包中的命名沖突

通過上面的代碼我們能看到有一個容器函數(shù)內(nèi)的名為 num 的變量以及一個嵌套函數(shù)內(nèi)同樣名為 num 的變量。這樣的執(zhí)行代碼結(jié)果以嵌套函數(shù)內(nèi)的變量優(yōu)先育叁⊙钙辏可能這里說成就近原則更容易記得住。這個就是閉包在實際應(yīng)用中應(yīng)該注意的一點豪嗽。

四谴蔑、閉包在開發(fā)中的應(yīng)用。

關(guān)于閉包在開發(fā)中的使用龟梦,最多的體現(xiàn)應(yīng)該還是在 Javascript 插件的開發(fā)上面隐锭。使用閉包可以避免變量污染。也就是說你在閉包中使用的變量名稱不會影響到其他地方同樣名稱,換個角度來講计贰,我將我嵌套函數(shù)內(nèi)部的變量給保護起來了钦睡,外部沒辦法隨便修改我內(nèi)部定義的變了。也就是雖然名字一樣但是你是你我是我躁倒。代碼體現(xiàn)如下:

function A(){
    function B(num){
        var c = 10;//內(nèi)部變量 c
        return num + c;
    }
    return B;
}

var c = 20;//外部變量c
var result = A()(c);
console.log(c);//20 
console.log(result)//30 

以上特點應(yīng)用在插件開發(fā)中就可以很好的保護了插件本身荞怒,避免了外界的串改,保證了插件的穩(wěn)定秧秉。

簡單的插件

初步代碼

//編寫插件代碼
var plugin = (function(){
    function SayHi(str = '你好啊!'){
        console.log(str);
    }
    return SayHi;
})();

//使用插件
plugin('hello');
plugin();
插件初步

上面代碼閉包部分我就不在累述了褐桌,我們來看看新出現(xiàn)的一種語法--自調(diào)用匿名函數(shù):

(function{
    //code
})();

實際作用是創(chuàng)建了一個匿名函數(shù),并在創(chuàng)建后立即執(zhí)行一次象迎。作用等價于下面的代碼荧嵌,唯一的區(qū)別就是下面的函數(shù)不是匿名的呛踊。

//創(chuàng)建
var func = function(){
    //code
}    
//調(diào)用
func();

當(dāng)然,我們編寫插件不可能只提供一個API給外部使用啦撮,如何返回多個API,我們這里使用字面量形式返回谭网。改進之后的代碼如下

//編寫插件代碼
var plugin = (function(){
    var _sayhi = function(str = '你好啊!'){
        console.log(str);
    }
    var _sayhello = function(){
        console.log("這個API能做很牛逼的事情");
    }
    return {
        SayHi : _sayhi,
        SayHello : _sayhello
    }
})();

//通過插件提供的API使用插件
plugin.SayHi('hello');
plugin.SayHello();

執(zhí)行結(jié)果

小改進

五、后語

今天對于閉包的看法暫時先寫到這了赃春,秉承著學(xué)以致用的原則愉择,下兩篇文章我將介紹 javascript 插件的幾種開發(fā)形式,以及實踐--開發(fā)一個原生的 Javascript 插件织中。

六薄辅、補充

技術(shù)因交流而進步,感謝各位提供意見的博友抠璃。寫文章的目的就是為了給正在學(xué)習(xí)階段的朋友一些參考站楚,當(dāng)然我最怕給的是誤導(dǎo)。針對博友指出的東西我在這里補充一下搏嗡,希望不會打亂你們之前對閉包的理解窿春。所以請將上述知識點理解為“從實踐角度理解的閉包”,其實關(guān)于 JS 閉包的概念我覺得可以從廣義和狹義(實踐)上來理解,既然有朋友提到了我們就繼續(xù)延伸一下。先來看看《JavaScript高級程序設(shè)計》第三版 中對閉包的定義

閉包是定義在一個外部函數(shù)內(nèi)部采盒,并且能夠訪問(存染善颉)外部函數(shù)中自由變量的函數(shù)”

按這樣的定義那么久必須是在嵌套函數(shù)的前提下才能形成閉包。我們上文所講的例子都是基于這一種磅氨。

我們再來看下另一個閉包的定義

閉包尺栖,是指語法域位于某個特定的區(qū)域迂猴,具有持續(xù)參照(讀寫)位于該區(qū)域內(nèi)自身范圍之外的執(zhí)行域上的非持久型變量值能力的段落

感覺挺抽象卑吭,沒關(guān)系搓劫。我們來看下下面代碼

var a = 10;
function A(){
    console.log(a);//10(讀的能力)
    a = 20;//(寫的能力)
    console.log(a);//20 
}
A();

通過代碼我們從新來看這一段定義閉包的文字

“閉包蹲坷,是指語法域位于某個特定的區(qū)域(A 函數(shù)),具有持續(xù)參照(讀寫)位于該區(qū)域內(nèi)自身范圍之外的執(zhí)行域上的非持久型變量(上述的變量 a)值能力的段落”

那么這樣子定義的話曹锨,Javascript 中的所有函數(shù)都有這樣的能力谬莹。所以也就有了這樣的說法:
Javascript 中的函數(shù)都是一個閉包诅迷。

關(guān)于閉包窃祝,本文先更新到這里掐松。其實閉包涉及的基礎(chǔ)知識比較多,有些許基礎(chǔ)(比如執(zhí)行上下文粪小,變量對象以及funarg 等)知識點本文還沒有提及大磺,后續(xù)文章會有所補充。

限于筆者技術(shù)探膊,文章觀點難免有不當(dāng)之處杠愧,希望發(fā)現(xiàn)問題的朋友幫忙指正,筆者將會及時更新突想。也請轉(zhuǎn)載的朋友注明文章出處并附上原文鏈接殴蹄,以便讀者能及時獲取到文章更新后的內(nèi)容,以免誤導(dǎo)讀者猾担。筆者力求避免寫些晦澀難懂的文章(雖然也有人說這樣顯得高逼格袭灯,專業(yè)),盡量使用簡單的用詞和例子來幫助理解绑嘹。如果表達(dá)上有好的建議的話也希望朋友們在評論處指出稽荧。

本文為作者原創(chuàng),轉(zhuǎn)載請注明出處! 東野文然《淺析 JavaScript 中的閉包(Closures)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市工腋,隨后出現(xiàn)的幾起案子姨丈,更是在濱河造成了極大的恐慌,老刑警劉巖擅腰,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蟋恬,死亡現(xiàn)場離奇詭異,居然都是意外死亡趁冈,警方通過查閱死者的電腦和手機歼争,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渗勘,“玉大人沐绒,你說我怎么就攤上這事⊥梗” “怎么了乔遮?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長取刃。 經(jīng)常有香客問我蹋肮,道長,這世上最難降的妖魔是什么璧疗? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任括尸,我火速辦了婚禮,結(jié)果婚禮上病毡,老公的妹妹穿的比我還像新娘濒翻。我一直安慰自己,他們只是感情好啦膜,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布有送。 她就那樣靜靜地躺著,像睡著了一般僧家。 火紅的嫁衣襯著肌膚如雪雀摘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天八拱,我揣著相機與錄音阵赠,去河邊找鬼涯塔。 笑死,一個胖子當(dāng)著我的面吹牛清蚀,可吹牛的內(nèi)容都是我干的匕荸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼枷邪,長吁一口氣:“原來是場噩夢啊……” “哼榛搔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起东揣,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤践惑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嘶卧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尔觉,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年芥吟,在試婚紗的時候發(fā)現(xiàn)自己被綠了穷娱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡运沦,死狀恐怖泵额,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情携添,我是刑警寧澤嫁盲,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站烈掠,受9級特大地震影響羞秤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜左敌,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一瘾蛋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧矫限,春花似錦哺哼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至无宿,卻和暖如春茵汰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背孽鸡。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工蹂午, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留栏豺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓豆胸,卻偏偏與公主長得像奥洼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子配乱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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