繼續(xù)一個(gè)人自言自語(yǔ)_。
今天想聊聊 JavaScript 的作用域,以及“閉包”。當(dāng)然,仍舊帶著我的個(gè)人特色。
作用域
JavaScript 據(jù)我了解只有一種作用域,叫做“函數(shù)作用域”,先看栗子:
var a = "a-out";
(function () {
console.log(a);
var a = "a-in";
})();
你覺得上面匿名函數(shù)執(zhí)行后,會(huì)輸出什么?
實(shí)際試下,會(huì)發(fā)現(xiàn)是undefined,這個(gè)我慢慢來(lái)說吧。
首先,JavaScript 沒有“塊級(jí)作用域”,不能在花括號(hào)中定義“局部變量”。所有的變量的作用域都是函數(shù)級(jí)的,也就是說,在函數(shù)內(nèi)部任意的地方聲明的變量,在函數(shù)內(nèi)的任意位置都可以使用。
當(dāng)然,例外就是,不在任何函數(shù)內(nèi)部的變量,就成了“全局變量”啦。同樣,在函數(shù)內(nèi)部,忘記使用 var 聲明就直接使用的變量,也會(huì)成為全局變量,所以很多時(shí)候會(huì)把函數(shù)內(nèi)部要使用的變量都在頭部進(jìn)行顯式聲明,以盡量避免麻煩。
回過頭來(lái)看上面的栗子:
第一行,聲明了一個(gè)全局變量
a,然后進(jìn)行了賦值。匿名函數(shù)的第一行,向控制臺(tái)輸出變量
a的值,由于函數(shù)內(nèi)部的確聲明了a,所以這里輸出內(nèi)部變量a的值。但是對(duì)內(nèi)部變量a的賦值在當(dāng)前行的后面,目前該變量沒有值,所以輸出的是undefined。匿名函數(shù)的第二行,聲明了內(nèi)部變量
a,然后進(jìn)行了賦值。
對(duì)于這件事情我的理解:
既然 JavaScript 只有“函數(shù)作用域”(我說的),所以腳本解釋器會(huì)先掃描下函數(shù)內(nèi)部,識(shí)別出所有的聲明的變量,記錄下來(lái)。然后,在執(zhí)行函數(shù)的這個(gè)階段,遇到一個(gè)“變量名”,就先在函數(shù)內(nèi)部的變量列表里面進(jìn)行查找,找到了就把這個(gè)內(nèi)部變量的當(dāng)前值交給當(dāng)前語(yǔ)句使用(當(dāng)然如果是賦值語(yǔ)句,則為變量“綁定”了一個(gè)新的值)。
那么,如果在函數(shù)內(nèi)部,使用一個(gè)變量時(shí),該變量并沒有在函數(shù)內(nèi)部聲明呢?
我把上面的栗子修改下:
var a = "a-out";
(function () {
console.log(a);
// var a = "a-in";
})();
試一下,會(huì)發(fā)現(xiàn)這里匿名函數(shù)打印出的是"a-out"。所以,在函數(shù)內(nèi)部沒有聲明這個(gè)變量的話,就在函數(shù)的外部來(lái)找啦。對(duì)于多級(jí)嵌套的函數(shù)來(lái)說,就該是一層一層往外找,直到找到同名的變量,或者到“頂層”也找不到就停下來(lái)(這個(gè)情況下如果執(zhí)行函數(shù)就出錯(cuò)了,提示變量沒有定義)。
當(dāng)然,這個(gè)查找變量的過程并不直觀,僅僅是我的描述而已,希望能幫你理解而不是相反。
對(duì)于函數(shù)的參數(shù),我也看作是函數(shù)的內(nèi)部變量(我不用“局部變量”的說法,但我想你大概知道我指的是什么),不過其值是要等到函數(shù)執(zhí)行時(shí)才能確定的。
把函數(shù)的定義,和函數(shù)的執(zhí)行分開來(lái)看。
函數(shù)定義時(shí),是在一個(gè)靜態(tài)的環(huán)境下,函數(shù)的內(nèi)外部的環(huán)境是固定的。盡管有很多變量的值還在變動(dòng),甚至只在每次執(zhí)行時(shí)才能確定,但這并不妨礙我們?nèi)ァ耙谩彼T诤瘮?shù)執(zhí)行的過程中,在每個(gè)具體的引用到變量的位置,都會(huì)被變量當(dāng)時(shí)的值所替代(同樣,聲明語(yǔ)句例外,是重新賦值)。
特別地,在函數(shù)定義階段(或者說解釋器解析而非執(zhí)行函數(shù)時(shí)),能夠使用哪些變量,也是確定的了。例如,函數(shù)內(nèi)部聲明了一個(gè) a 變量,那么函數(shù)內(nèi)部使用到 a 變量的語(yǔ)句,就是跟這個(gè) a 關(guān)聯(lián)的了。而如果內(nèi)部沒有,則在一層層的外部作用域(外部函數(shù)的作用域,如果有的話)里查找,如果還沒有的話呢?那么你執(zhí)行函數(shù)時(shí)就報(bào)錯(cuò)了唄。
接著這個(gè)話題,我們引出“閉包”。
閉包
對(duì)于“閉包”這樣嚴(yán)肅的東西,還是來(lái)看維基百科的定義:
在計(jì)算機(jī)科學(xué)中,閉包(Closure)是詞法閉包(Lexical Closure)的簡(jiǎn)稱,是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體。閉包在運(yùn)行時(shí)可以有多個(gè)實(shí)例,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實(shí)例。
來(lái)看一個(gè)栗子:
var count = (function () {
var i = 0;
return function () {
return ++i;
};
})();
count(); // 1
count(); // 2
在這個(gè)栗子中,有兩個(gè)匿名函數(shù),其中一個(gè)嵌套在另一個(gè)的內(nèi)部。外層的匿名函數(shù)被立即執(zhí)行了(通過 (function () {})() 的方式),然后的是內(nèi)部的匿名函數(shù),被作為返回值賦給了變量 count。所以,經(jīng)過上面的過程,count 的值是一個(gè)函數(shù)對(duì)象。
而 count 所關(guān)聯(lián)的這個(gè)函數(shù)比較特別,在定義時(shí),它引用了外部函數(shù)的變量 i,于是,外部變量 i 和這個(gè)函數(shù)構(gòu)成了上面定義中的“閉包”,也就產(chǎn)生了上面的現(xiàn)象。
那么這一切,和這個(gè)詭異的名字一起,有什么作用呢?只是讓人覺得迷惑,或者讓不熟悉的人大呼“NB”?
我曾經(jīng)看到過一種說法,是說使用閉包是為了在 JavaScript 中提供一種使用局部變量的機(jī)制。且不論這個(gè)對(duì)于閉包的評(píng)價(jià)本身正確與否,我們來(lái)試著理解下“局部變量”這回事。還是舉個(gè)栗子:
function Person(name) {
this.name = name;
this.getName = function () {
return this.name;
};
}
假設(shè)我們希望,其他人在使用到這個(gè) Person 類(暫且叫做“類”吧,后面我想專門寫一篇東西來(lái)聊聊 JavaScript 的繼承和類)時(shí),只能通過 getName() 來(lái)獲取 name,而不能直接訪問 name 并改變其值的話,我感覺不太現(xiàn)實(shí)。因?yàn)?JavaScript 沒有提供像 private, public 這樣的東東,所有的東西都默認(rèn)是公開的。而如果非要這樣做,畢竟這樣做也是有著合理的應(yīng)用場(chǎng)景的,通??梢酝ㄟ^閉包來(lái)嚴(yán)格實(shí)現(xiàn)(只是把屬性名改為類似 _name 這樣來(lái)提示他人不要亂改,畢竟不嚴(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;
};
}
我們來(lái)使用下這個(gè)“類”:
var me = new Person("luobo");
me.getName(); // "luobo"
顯然,沒有 name 屬性,也就無(wú)法直接修改這個(gè)值啦。(當(dāng)然,如果要作為“私有”成員使用的是對(duì)象,那么即便采用上述方法,由于返回的是對(duì)象本身,所以仍舊可以修改對(duì)象)
回到上面的問題,關(guān)于閉包的作用,我還是覺得:閉包是語(yǔ)言本身提供的一種機(jī)制,并不見得就一定是為了什么特定目的而創(chuàng)造的,更加不會(huì)是為了創(chuàng)造“局部變量”的這一個(gè)目的。根據(jù)自己的需要,在合適的地方使用它就是了,只要你是真的會(huì)用就好_。
小結(jié)
抱歉,今天情緒不佳,寫東西不是很有激情,所以上面的文字盡管我的確花了心思,但自己都不太滿意。作用域和“閉包”是我理解的 JavaScript 中的一個(gè)很重要的主題,花了很長(zhǎng)時(shí)間我才有了上面的那些體會(huì),但是敘述地有點(diǎn)沒有頭緒啦。
關(guān)于作用域,我認(rèn)為先要對(duì)于 JavaScript 代碼的執(zhí)行有一定的理解。(我說說我的理解吧,歡迎交流。)在瀏覽器環(huán)境下,沒有寫在任何函數(shù)內(nèi)部的語(yǔ)句,就直接執(zhí)行了。寫在函數(shù)內(nèi)部的代碼,則只有等到函數(shù)被調(diào)用的時(shí)候,才會(huì)執(zhí)行。但瀏覽器雖然沒有執(zhí)行函數(shù),還是會(huì)先把函數(shù)“讀”一遍,“理解”了之后記錄下來(lái)。這樣當(dāng)任何時(shí)候需要使用這個(gè)函數(shù)的時(shí)候,就能直接拿來(lái),結(jié)合當(dāng)時(shí)的環(huán)境來(lái)執(zhí)行啦。
這個(gè)過程的細(xì)節(jié)中就有關(guān)作用域、閉包的身影啦。
當(dāng)然上面是我個(gè)人的理解,描述也不夠準(zhǔn)確和正確。但是我想對(duì)于函數(shù)的定義和執(zhí)行的機(jī)制有更深入的理解還是很必要的,特別是想真的對(duì) JavaScript 這門語(yǔ)言有更深入的理解的話。顯然,我也還需要加油?。?/p>
關(guān)于“閉包”,這真的是一個(gè)“高級(jí)”的話題。而且,在不領(lǐng)會(huì)閉包的原理的情況下,很有可能不知不覺就給自己挖了一個(gè)坑出來(lái),我就干過。
var arr = ['a', 'b', 'c'], funcs = [];
for (var i = 0, len = arr.length; i < len; i++) {
funcs[i] = function () {
return arr[i];
};
}
上面這個(gè)造作的栗子中,我的本意是:得到一個(gè)數(shù)組 funcs,該數(shù)組的每一項(xiàng)都是一個(gè)返回?cái)?shù)組 arr 中對(duì)應(yīng)位置的值的函數(shù)。有點(diǎn)繞,不過我想你能明白是什么意思。但是,結(jié)果卻并非如此:
funcs[1](); // undefined
奇怪,不應(yīng)該返回 arr[1] 也就是 'b' 嗎?
曾經(jīng)我為此煩惱過....后來(lái)我意識(shí)到,我不知不覺用閉包給自己挖了這個(gè)坑。
如果你還沒有看明白(當(dāng)然,我的敘述本身就亂,難為你了),我來(lái)給些提示:
- 試著在控制臺(tái)輸出變量
i,看下當(dāng)前值 - 然后在控制臺(tái)輸出
arr[i],看下當(dāng)前值 - for 循環(huán)中,其實(shí)每次構(gòu)建的匿名函數(shù),返回的就是
arr[i] - 通過
i = 1把變量i的值改為 1 - 再執(zhí)行下上面的
funcs[1](),或者funcs[0]()funcs[2]()都一樣,看下結(jié)果你應(yīng)該就能明白了....
好吧,這就是閉包。