繼續一個人自言自語_。
今天想聊聊 JavaScript 的作用域,以及“閉包”。當然,仍舊帶著我的個人特色。
作用域
JavaScript 據我了解只有一種作用域,叫做“函數作用域”,先看栗子:
var a = "a-out";
(function () {
console.log(a);
var a = "a-in";
})();
你覺得上面匿名函數執行后,會輸出什么?
實際試下,會發現是undefined
,這個我慢慢來說吧。
首先,JavaScript 沒有“塊級作用域”,不能在花括號中定義“局部變量”。所有的變量的作用域都是函數級的,也就是說,在函數內部任意的地方聲明的變量,在函數內的任意位置都可以使用。
當然,例外就是,不在任何函數內部的變量,就成了“全局變量”啦。同樣,在函數內部,忘記使用 var
聲明就直接使用的變量,也會成為全局變量,所以很多時候會把函數內部要使用的變量都在頭部進行顯式聲明,以盡量避免麻煩。
回過頭來看上面的栗子:
第一行,聲明了一個全局變量
a
,然后進行了賦值。匿名函數的第一行,向控制臺輸出變量
a
的值,由于函數內部的確聲明了a
,所以這里輸出內部變量a
的值。但是對內部變量a
的賦值在當前行的后面,目前該變量沒有值,所以輸出的是undefined
。匿名函數的第二行,聲明了內部變量
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]()
都一樣,看下結果你應該就能明白了....
好吧,這就是閉包。