JavaScript - 作用域和“閉包”

繼續(xù)一個人自言自語_聂示。

今天想聊聊 JavaScript 的作用域它掂,以及“閉包”彬檀。當(dāng)然崔拥,仍舊帶著我的個人特色。

作用域

JavaScript 據(jù)我了解只有一種作用域凤覆,叫做“函數(shù)作用域”链瓦,先看栗子:

var a = "a-out";
(function () {
    console.log(a);
    var a = "a-in";
})();

你覺得上面匿名函數(shù)執(zhí)行后,會輸出什么盯桦?

實際試下慈俯,會發(fā)現(xiàn)是undefined,這個我慢慢來說吧拥峦。

首先贴膘,JavaScript 沒有“塊級作用域”,不能在花括號中定義“局部變量”略号。所有的變量的作用域都是函數(shù)級的刑峡,也就是說,在函數(shù)內(nèi)部任意的地方聲明的變量玄柠,在函數(shù)內(nèi)的任意位置都可以使用突梦。

當(dāng)然,例外就是羽利,不在任何函數(shù)內(nèi)部的變量宫患,就成了“全局變量”啦。同樣这弧,在函數(shù)內(nèi)部娃闲,忘記使用 var 聲明就直接使用的變量,也會成為全局變量匾浪,所以很多時候會把函數(shù)內(nèi)部要使用的變量都在頭部進(jìn)行顯式聲明皇帮,以盡量避免麻煩。

回過頭來看上面的栗子:

  1. 第一行蛋辈,聲明了一個全局變量 a属拾,然后進(jìn)行了賦值。

  2. 匿名函數(shù)的第一行,向控制臺輸出變量 a 的值捌年,由于函數(shù)內(nèi)部的確聲明了 a,所以這里輸出內(nèi)部變量 a 的值挂洛。但是對內(nèi)部變量 a 的賦值在當(dāng)前行的后面礼预,目前該變量沒有值,所以輸出的是 undefined虏劲。

  3. 匿名函數(shù)的第二行托酸,聲明了內(nèi)部變量 a,然后進(jìn)行了賦值柒巫。

對于這件事情我的理解:

既然 JavaScript 只有“函數(shù)作用域”(我說的)励堡,所以腳本解釋器會先掃描下函數(shù)內(nèi)部,識別出所有的聲明的變量堡掏,記錄下來应结。然后,在執(zhí)行函數(shù)的這個階段泉唁,遇到一個“變量名”鹅龄,就先在函數(shù)內(nèi)部的變量列表里面進(jìn)行查找,找到了就把這個內(nèi)部變量的當(dāng)前值交給當(dāng)前語句使用(當(dāng)然如果是賦值語句亭畜,則為變量“綁定”了一個新的值)扮休。

那么,如果在函數(shù)內(nèi)部拴鸵,使用一個變量時玷坠,該變量并沒有在函數(shù)內(nèi)部聲明呢?

我把上面的栗子修改下:

var a = "a-out";
(function () {
    console.log(a);
    // var a = "a-in";
})();

試一下劲藐,會發(fā)現(xiàn)這里匿名函數(shù)打印出的是"a-out"八堡。所以,在函數(shù)內(nèi)部沒有聲明這個變量的話聘芜,就在函數(shù)的外部來找啦秕重。對于多級嵌套的函數(shù)來說,就該是一層一層往外找厉膀,直到找到同名的變量溶耘,或者到“頂層”也找不到就停下來(這個情況下如果執(zhí)行函數(shù)就出錯了,提示變量沒有定義)服鹅。

當(dāng)然凳兵,這個查找變量的過程并不直觀,僅僅是我的描述而已企软,希望能幫你理解而不是相反庐扫。

對于函數(shù)的參數(shù),我也看作是函數(shù)的內(nèi)部變量(我不用“局部變量”的說法,但我想你大概知道我指的是什么)形庭,不過其值是要等到函數(shù)執(zhí)行時才能確定的铅辞。

把函數(shù)的定義,和函數(shù)的執(zhí)行分開來看萨醒。

函數(shù)定義時斟珊,是在一個靜態(tài)的環(huán)境下,函數(shù)的內(nèi)外部的環(huán)境是固定的富纸。盡管有很多變量的值還在變動囤踩,甚至只在每次執(zhí)行時才能確定,但這并不妨礙我們?nèi)ァ耙谩彼省T诤瘮?shù)執(zhí)行的過程中堵漱,在每個具體的引用到變量的位置,都會被變量當(dāng)時的值所替代(同樣涣仿,聲明語句例外勤庐,是重新賦值)。

特別地好港,在函數(shù)定義階段(或者說解釋器解析而非執(zhí)行函數(shù)時)埃元,能夠使用哪些變量,也是確定的了媚狰。例如岛杀,函數(shù)內(nèi)部聲明了一個 a 變量,那么函數(shù)內(nèi)部使用到 a 變量的語句崭孤,就是跟這個 a 關(guān)聯(lián)的了类嗤。而如果內(nèi)部沒有,則在一層層的外部作用域(外部函數(shù)的作用域辨宠,如果有的話)里查找遗锣,如果還沒有的話呢?那么你執(zhí)行函數(shù)時就報錯了唄嗤形。

接著這個話題精偿,我們引出“閉包”。

閉包

對于“閉包”這樣嚴(yán)肅的東西赋兵,還是來看維基百科的定義:

在計算機(jī)科學(xué)中笔咽,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數(shù)霹期。這個被引用的自由變量將和這個函數(shù)一同存在叶组,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以历造,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體甩十。閉包在運(yùn)行時可以有多個實例船庇,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實例。

來看一個栗子:

var count = (function () {
    var i = 0;
    return function () {
        return ++i;
    };
})();

count(); // 1
count(); // 2

在這個栗子中侣监,有兩個匿名函數(shù)鸭轮,其中一個嵌套在另一個的內(nèi)部。外層的匿名函數(shù)被立即執(zhí)行了(通過 (function () {})() 的方式)橄霉,然后的是內(nèi)部的匿名函數(shù)窃爷,被作為返回值賦給了變量 count。所以酪劫,經(jīng)過上面的過程吞鸭,count 的值是一個函數(shù)對象寺董。

而 count 所關(guān)聯(lián)的這個函數(shù)比較特別覆糟,在定義時,它引用了外部函數(shù)的變量 i遮咖,于是滩字,外部變量 i 和這個函數(shù)構(gòu)成了上面定義中的“閉包”,也就產(chǎn)生了上面的現(xiàn)象御吞。

那么這一切麦箍,和這個詭異的名字一起,有什么作用呢陶珠?只是讓人覺得迷惑挟裂,或者讓不熟悉的人大呼“NB”?

我曾經(jīng)看到過一種說法揍诽,是說使用閉包是為了在 JavaScript 中提供一種使用局部變量的機(jī)制诀蓉。且不論這個對于閉包的評價本身正確與否,我們來試著理解下“局部變量”這回事暑脆。還是舉個栗子:

function Person(name) {
    this.name = name;
    this.getName = function () {
        return this.name;
    };
}

假設(shè)我們希望渠啤,其他人在使用到這個 Person 類(暫且叫做“類”吧,后面我想專門寫一篇東西來聊聊 JavaScript 的繼承和類)時添吗,只能通過 getName() 來獲取 name沥曹,而不能直接訪問 name 并改變其值的話,我感覺不太現(xiàn)實碟联。因為 JavaScript 沒有提供像 private, public 這樣的東東妓美,所有的東西都默認(rèn)是公開的。而如果非要這樣做鲤孵,畢竟這樣做也是有著合理的應(yīng)用場景的部脚,通常可以通過閉包來嚴(yán)格實現(xiàn)(只是把屬性名改為類似 _name 這樣來提示他人不要亂改裤纹,畢竟不嚴(yán)格不是):

function Person(name) {
    var _name = name;
    this.getName = function () {
        return _name;
    };
}

當(dāng)然委刘,前面提到過丧没,也可以把函數(shù)的參數(shù)看作是內(nèi)部變量,所以也可以直接這樣:

function Person(name) {
    this.getName = function () {
        return name;
    };
}

我們來使用下這個“類”:

var me = new Person("luobo");
me.getName(); // "luobo"

顯然锡移,沒有 name 屬性呕童,也就無法直接修改這個值啦。(當(dāng)然淆珊,如果要作為“私有”成員使用的是對象夺饲,那么即便采用上述方法,由于返回的是對象本身施符,所以仍舊可以修改對象)

回到上面的問題往声,關(guān)于閉包的作用,我還是覺得:閉包是語言本身提供的一種機(jī)制戳吝,并不見得就一定是為了什么特定目的而創(chuàng)造的浩销,更加不會是為了創(chuàng)造“局部變量”的這一個目的。根據(jù)自己的需要听哭,在合適的地方使用它就是了慢洋,只要你是真的會用就好_

小結(jié)

抱歉陆盘,今天情緒不佳普筹,寫東西不是很有激情,所以上面的文字盡管我的確花了心思隘马,但自己都不太滿意太防。作用域和“閉包”是我理解的 JavaScript 中的一個很重要的主題,花了很長時間我才有了上面的那些體會酸员,但是敘述地有點沒有頭緒啦蜒车。

關(guān)于作用域,我認(rèn)為先要對于 JavaScript 代碼的執(zhí)行有一定的理解沸呐。(我說說我的理解吧醇王,歡迎交流。)在瀏覽器環(huán)境下崭添,沒有寫在任何函數(shù)內(nèi)部的語句寓娩,就直接執(zhí)行了。寫在函數(shù)內(nèi)部的代碼呼渣,則只有等到函數(shù)被調(diào)用的時候棘伴,才會執(zhí)行。但瀏覽器雖然沒有執(zhí)行函數(shù)屁置,還是會先把函數(shù)“讀”一遍焊夸,“理解”了之后記錄下來。這樣當(dāng)任何時候需要使用這個函數(shù)的時候蓝角,就能直接拿來阱穗,結(jié)合當(dāng)時的環(huán)境來執(zhí)行啦饭冬。

這個過程的細(xì)節(jié)中就有關(guān)作用域、閉包的身影啦揪阶。

當(dāng)然上面是我個人的理解昌抠,描述也不夠準(zhǔn)確和正確。但是我想對于函數(shù)的定義和執(zhí)行的機(jī)制有更深入的理解還是很必要的鲁僚,特別是想真的對 JavaScript 這門語言有更深入的理解的話炊苫。顯然,我也還需要加油氨场侨艾!

關(guān)于“閉包”,這真的是一個“高級”的話題拓挥。而且唠梨,在不領(lǐng)會閉包的原理的情況下,很有可能不知不覺就給自己挖了一個坑出來撞叽,我就干過姻成。

var arr = ['a', 'b', 'c'], funcs = [];
for (var i = 0, len = arr.length; i < len; i++) {
    funcs[i] = function () {
        return arr[i];
    };
}

上面這個造作的栗子中插龄,我的本意是:得到一個數(shù)組 funcs愿棋,該數(shù)組的每一項都是一個返回數(shù)組 arr 中對應(yīng)位置的值的函數(shù)。有點繞均牢,不過我想你能明白是什么意思糠雨。但是,結(jié)果卻并非如此:

funcs[1](); // undefined

奇怪徘跪,不應(yīng)該返回 arr[1] 也就是 'b' 嗎甘邀?

曾經(jīng)我為此煩惱過....后來我意識到,我不知不覺用閉包給自己挖了這個坑垮庐。

如果你還沒有看明白(當(dāng)然松邪,我的敘述本身就亂,難為你了)哨查,我來給些提示:

  • 試著在控制臺輸出變量 i逗抑,看下當(dāng)前值
  • 然后在控制臺輸出 arr[i],看下當(dāng)前值
  • for 循環(huán)中寒亥,其實每次構(gòu)建的匿名函數(shù)邮府,返回的就是 arr[i]
  • 通過 i = 1 把變量 i 的值改為 1
  • 再執(zhí)行下上面的 funcs[1](),或者 funcs[0]() funcs[2]() 都一樣溉奕,看下結(jié)果你應(yīng)該就能明白了....

好吧褂傀,這就是閉包。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末加勤,一起剝皮案震驚了整個濱河市仙辟,隨后出現(xiàn)的幾起案子同波,更是在濱河造成了極大的恐慌,老刑警劉巖叠国,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件参萄,死亡現(xiàn)場離奇詭異,居然都是意外死亡煎饼,警方通過查閱死者的電腦和手機(jī)讹挎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吆玖,“玉大人筒溃,你說我怎么就攤上這事≌闯耍” “怎么了怜奖?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長翅阵。 經(jīng)常有香客問我歪玲,道長,這世上最難降的妖魔是什么掷匠? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任滥崩,我火速辦了婚禮,結(jié)果婚禮上讹语,老公的妹妹穿的比我還像新娘钙皮。我一直安慰自己,他們只是感情好顽决,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布短条。 她就那樣靜靜地躺著,像睡著了一般才菠。 火紅的嫁衣襯著肌膚如雪茸时。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天赋访,我揣著相機(jī)與錄音可都,去河邊找鬼。 笑死进每,一個胖子當(dāng)著我的面吹牛汹粤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播田晚,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼嘱兼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贤徒?” 一聲冷哼從身側(cè)響起芹壕,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤汇四,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后踢涌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝎困,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡偿凭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年辟癌,在試婚紗的時候發(fā)現(xiàn)自己被綠了歼郭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡潘明,死狀恐怖行剂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钳降,我是刑警寧澤厚宰,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站遂填,受9級特大地震影響铲觉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吓坚,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一撵幽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凌唬,春花似錦并齐、人聲如沸漏麦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撕贞。三九已至更耻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捏膨,已是汗流浹背秧均。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留号涯,地道東北人目胡。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像链快,于是被迫代替她去往敵國和親誉己。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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