去年我寫了一篇“closures的簡介”,它的目的是幫助大家理解‘什么是閉包,閉包是如何工作的’〗寥梗現(xiàn)在我嘗試從另外一個不同的角度去闡釋閉包。有了這些基本的概念裹芝,你只需要盡可能多地閱讀這些解釋部逮,來更全面地理解閉包。
First-class functions
就像我在“Why JavaScript is AWESOME”中解釋的那樣嫂易,JavaScript的強大之處的一部分來自于它的’first-class functions‘甥啄。那么編程語言中的’first-class‘意味著什么?
可以被存放在變量和數(shù)據(jù)結(jié)構(gòu)中
可以作為子例程的參數(shù)被傳遞
可以作為子例程的返回值被返回
可以在運行時被構(gòu)造
有固有的id(區(qū)別于任何給定的名字)
所以炬搭,JavaScript中的functions非常類似objects蜈漓。事實上,在JavaScript中functions就是objects宫盔。能夠嵌套使用函數(shù)融虽,讓我們可以使用閉包,這也是我接下來要討論的...
Nested functions(嵌套函數(shù))
如下是嵌套函數(shù)的一個小例子:
http://jsfiddle.net/skilldrick/66jFm/embedded/
在這兒要注意的重要事情是只有唯一的一個f函數(shù)被定義灼芭。每次函數(shù)f被調(diào)用后有额,一個新的函數(shù)g被創(chuàng)建,(函數(shù)g)局部于函數(shù)f的執(zhí)行過程中彼绷。當函數(shù)g被返回時巍佑,我們可以把它賦值給一個全局變量。所以寄悯,我們可以調(diào)用f函數(shù)萤衰,把結(jié)果賦值給變量g5;接著我們再一次調(diào)用f函數(shù)猜旬,并把結(jié)果賦值給變量g1脆栋。g1和g5是2個不同的函數(shù)倦卖,但碰巧的是它們共享著同一份代碼,只不過它們他們在不同的上下文中被執(zhí)行椿争,使用著不同的‘free variable(自由變量)’怕膛。(題外話,使用函數(shù)定義去定義‘函數(shù)g’秦踪,接著返回函數(shù)g褐捻,并不是必需的。我們可以使用函數(shù)表達式作為替代椅邓,函數(shù)表達式允許我們創(chuàng)建函數(shù)時不用命名函數(shù)柠逞。也就是直接返回匿名函數(shù)。這兒是使用匿名函數(shù)替換后的版本)
Free variables and scope(自由變量和作用域)
如果一個變量在包含它的作用域中被定義希坚,那么該變量在包含它的作用域內(nèi)的任何其它作用域內(nèi)都是自由的(原文:A variable is free in any particular scope if it is defined within an enclosing scope.)。為了表達的更具體陵且,也就是:在函數(shù)g的作用域內(nèi)裁僧,變量x是自由的。因為變量x是在函數(shù)f的作用域內(nèi)被定義的(而且‘作用域f’包含‘作用域g’)慕购。同樣地聊疲,任何全局變量在‘作用域f’和’作用域g‘內(nèi)也是自由的。
作用域意味著什么沪悲?一個作用域是一個代碼區(qū)获洲,在該代碼區(qū)中可以定義變量,并且包圍該作用域的外圍作用域不能訪問該作用域內(nèi)的變量(原文:A scope is an area of code where a variable may be defined, without the enclosing scope knowing about it.)殿如。JavaScript有‘函數(shù)作用域’贡珊,所以函數(shù)有它自己的作用域。所以在‘函數(shù)f’中定義的任何變量涉馁,外部都是看不到的门岔。作用域是可以嵌套的,所以烤送,在上述例子中寒随,函數(shù)g有它自己的作用域,函數(shù)g的作用域被函數(shù)f包圍著帮坚,函數(shù)f的作用域被全局作用域包圍著妻往。當一個變量被訪問時,JavaScript解釋器在當前作用域內(nèi)查找變量试和,如果在當前作用域內(nèi)找不到該變量的定義讯泣,解釋器會查看包圍著當前作用域的作用域,接著是查看爺爺作用域阅悍,一直向上直到全局作用域判帮。如果此時局嘁,變量的定義仍未被找到,一個ReferenceError異常就會被拋出晦墙。
Closures are functions that retain a reference to their free variables(閉包是‘保留著它們的自由變量的一份引用’的函數(shù))
并且這是問題的本質(zhì)悦昵。讓我們先看下上述例子的一個簡化版本:
http://jsfiddle.net/skilldrick/XDrsn/embedded/
調(diào)用函數(shù)f時傳遞一個參數(shù)5,執(zhí)行函數(shù)f時晌畅,函數(shù)g會被調(diào)用但指。當函數(shù)g被調(diào)用時,函數(shù)g可以訪問那個形參x抗楔,這并沒有什么奇怪的棋凳。令人驚訝的地方在于,當你從函數(shù)f中返回函數(shù)g后连躏,返回的函數(shù)g在被調(diào)用時仍然可以訪問你傳遞的參數(shù)5(就像原先那個例子中展示的那樣)剩岳。讓人迷惑的地方在于:函數(shù)g被返回后,仍然記得在函數(shù)f被調(diào)用時被定義的變量x(這也是大家理解閉包時入热,有困惑的地方)拍棕。從這點來說,確實不能理解勺良。那么看看下面的例子:
http://jsfiddle.net/skilldrick/DEUdF/embedded/
當person被調(diào)用绰播,形參name一定是被傳遞的值。所以尚困,person第一次被調(diào)用時蠢箩,name一定是‘Dave’,person第二次被調(diào)用事甜,name一定是‘Mary’谬泌。person定義了2個內(nèi)部函數(shù)‘set和get’。當這些函數(shù)(set和get)第一次被定義時逻谦,它們有一個‘自由變量name’呵萨,并且name的值一定是’Dave‘。這2個函數(shù)被數(shù)組包裹著返回跨跨,在外部被取出并賦值給2個變量’getDave和setDave‘潮峦。(如果你想從函數(shù)中返回一個以上的值,你要么返回一個對象勇婴,要么返回一個數(shù)組忱嘹。在這里使用數(shù)組顯得有點啰嗦,但是如果使用對象的話會混淆我們討論的問題耕渴。這兒又一個使用對象的版本拘悦,如果它對你來說更好理解的話)
并且這就是神奇的地方,getDave和setDave都記得同一個變量name(name初始時一定是Dave)橱脸。當setDave(’Bob‘)被調(diào)用時础米,變量name被設(shè)置為’Bob‘》治現(xiàn)在getDave被調(diào)用,它返回了’Bob‘屁桑。所以getDave和setDave這兩個函數(shù)記得同一個變量医寿。這也就是我想表達的含義:’閉包是保留它們自由變量的一份引用的函數(shù)‘。getDave和setDave都記得它們共有的自由變量name蘑斧。即使person已經(jīng)返回靖秩,但是變量name繼續(xù)存活,因為變量name被getDave和setDave引用著竖瘾。
當person第一次被調(diào)用時沟突,變量name一定是’Dave‘。當person第二次被調(diào)用時捕传,變量name的一份新版本被創(chuàng)建惠拭,當然get和set也被新建了一份。所以getMary庸论,setMary函數(shù)和getDave职辅,setDave是完全不同的。雖然它們執(zhí)行著同樣的代碼葡公,但是它們的上下文環(huán)境不同罐农,有著不同的自由變量条霜。
Summary總結(jié)
總的來說催什,閉包是一個函數(shù)’該函數(shù)在一個上下文中被調(diào)用,(該函數(shù))卻記得在另一個上下文中定義的變量‘(也就是該函數(shù)被定義的上下文)宰睡。在同一個上下文中定義的多個閉包記得同樣的上下文蒲凶,所以任何一個閉包修改上下文,其他閉包也會受影響(因為多個閉包共享同一個上下文拆内,就像上面例子顯示的那樣 setDave('Bob')后 getDave()也會受到影響)旋圆。
本文翻譯自:http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/
轉(zhuǎn)載請注明出處