繼續(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)行顯式聲明皇帮,以盡量避免麻煩。
回過頭來看上面的栗子:
第一行蛋辈,聲明了一個全局變量
a
属拾,然后進(jìn)行了賦值。匿名函數(shù)的第一行,向控制臺輸出變量
a
的值捌年,由于函數(shù)內(nèi)部的確聲明了a
,所以這里輸出內(nèi)部變量a
的值挂洛。但是對內(nèi)部變量a
的賦值在當(dāng)前行的后面礼预,目前該變量沒有值,所以輸出的是undefined
虏劲。匿名函數(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)該就能明白了....
好吧褂傀,這就是閉包。