JavaScript - 作用域和“閉包”

繼續一個人自言自語_

今天想聊聊 JavaScript 的作用域,以及“閉包”。當然,仍舊帶著我的個人特色。

作用域

JavaScript 據我了解只有一種作用域,叫做“函數作用域”,先看栗子:

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

你覺得上面匿名函數執行后,會輸出什么?

實際試下,會發現是undefined,這個我慢慢來說吧。

首先,JavaScript 沒有“塊級作用域”,不能在花括號中定義“局部變量”。所有的變量的作用域都是函數級的,也就是說,在函數內部任意的地方聲明的變量,在函數內的任意位置都可以使用。

當然,例外就是,不在任何函數內部的變量,就成了“全局變量”啦。同樣,在函數內部,忘記使用 var 聲明就直接使用的變量,也會成為全局變量,所以很多時候會把函數內部要使用的變量都在頭部進行顯式聲明,以盡量避免麻煩。

回過頭來看上面的栗子:

  1. 第一行,聲明了一個全局變量 a,然后進行了賦值。

  2. 匿名函數的第一行,向控制臺輸出變量 a 的值,由于函數內部的確聲明了 a,所以這里輸出內部變量 a 的值。但是對內部變量 a 的賦值在當前行的后面,目前該變量沒有值,所以輸出的是 undefined

  3. 匿名函數的第二行,聲明了內部變量 a,然后進行了賦值。

對于這件事情我的理解:

既然 JavaScript 只有“函數作用域”(我說的),所以腳本解釋器會先掃描下函數內部,識別出所有的聲明的變量,記錄下來。然后,在執行函數的這個階段,遇到一個“變量名”,就先在函數內部的變量列表里面進行查找,找到了就把這個內部變量的當前值交給當前語句使用(當然如果是賦值語句,則為變量“綁定”了一個新的值)。

那么,如果在函數內部,使用一個變量時,該變量并沒有在函數內部聲明呢?

我把上面的栗子修改下:

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

試一下,會發現這里匿名函數打印出的是"a-out"。所以,在函數內部沒有聲明這個變量的話,就在函數的外部來找啦。對于多級嵌套的函數來說,就該是一層一層往外找,直到找到同名的變量,或者到“頂層”也找不到就停下來(這個情況下如果執行函數就出錯了,提示變量沒有定義)。

當然,這個查找變量的過程并不直觀,僅僅是我的描述而已,希望能幫你理解而不是相反。

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

把函數的定義,和函數的執行分開來看。

函數定義時,是在一個靜態的環境下,函數的內外部的環境是固定的。盡管有很多變量的值還在變動,甚至只在每次執行時才能確定,但這并不妨礙我們去“引用”它。在函數執行的過程中,在每個具體的引用到變量的位置,都會被變量當時的值所替代(同樣,聲明語句例外,是重新賦值)。

特別地,在函數定義階段(或者說解釋器解析而非執行函數時),能夠使用哪些變量,也是確定的了。例如,函數內部聲明了一個 a 變量,那么函數內部使用到 a 變量的語句,就是跟這個 a 關聯的了。而如果內部沒有,則在一層層的外部作用域(外部函數的作用域,如果有的話)里查找,如果還沒有的話呢?那么你執行函數時就報錯了唄。

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

閉包

對于“閉包”這樣嚴肅的東西,還是來看維基百科的定義:

在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。

來看一個栗子:

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

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

在這個栗子中,有兩個匿名函數,其中一個嵌套在另一個的內部。外層的匿名函數被立即執行了(通過 (function () {})() 的方式),然后的是內部的匿名函數,被作為返回值賦給了變量 count。所以,經過上面的過程,count 的值是一個函數對象。

而 count 所關聯的這個函數比較特別,在定義時,它引用了外部函數的變量 i,于是,外部變量 i 和這個函數構成了上面定義中的“閉包”,也就產生了上面的現象。

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

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

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

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

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

當然,前面提到過,也可以把函數的參數看作是內部變量,所以也可以直接這樣:

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

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

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

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

回到上面的問題,關于閉包的作用,我還是覺得:閉包是語言本身提供的一種機制,并不見得就一定是為了什么特定目的而創造的,更加不會是為了創造“局部變量”的這一個目的。根據自己的需要,在合適的地方使用它就是了,只要你是真的會用就好_

小結

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

關于作用域,我認為先要對于 JavaScript 代碼的執行有一定的理解。(我說說我的理解吧,歡迎交流。)在瀏覽器環境下,沒有寫在任何函數內部的語句,就直接執行了。寫在函數內部的代碼,則只有等到函數被調用的時候,才會執行。但瀏覽器雖然沒有執行函數,還是會先把函數“讀”一遍,“理解”了之后記錄下來。這樣當任何時候需要使用這個函數的時候,就能直接拿來,結合當時的環境來執行啦。

這個過程的細節中就有關作用域、閉包的身影啦。

當然上面是我個人的理解,描述也不夠準確和正確。但是我想對于函數的定義和執行的機制有更深入的理解還是很必要的,特別是想真的對 JavaScript 這門語言有更深入的理解的話。顯然,我也還需要加油啊!

關于“閉包”,這真的是一個“高級”的話題。而且,在不領會閉包的原理的情況下,很有可能不知不覺就給自己挖了一個坑出來,我就干過。

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

上面這個造作的栗子中,我的本意是:得到一個數組 funcs,該數組的每一項都是一個返回數組 arr 中對應位置的值的函數。有點繞,不過我想你能明白是什么意思。但是,結果卻并非如此:

funcs[1](); // undefined

奇怪,不應該返回 arr[1] 也就是 'b' 嗎?

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

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

  • 試著在控制臺輸出變量 i,看下當前值
  • 然后在控制臺輸出 arr[i],看下當前值
  • for 循環中,其實每次構建的匿名函數,返回的就是 arr[i]
  • 通過 i = 1 把變量 i 的值改為 1
  • 再執行下上面的 funcs[1](),或者 funcs[0]() funcs[2]() 都一樣,看下結果你應該就能明白了....

好吧,這就是閉包。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容