JavaScript 中有一個很重要很重要很重要的概念就是閉包干发。重要的事情要說三遍绞愚!敲敲黑板叙甸,打瞌睡的同學醒醒,下面講的是重點位衩!
據說有很多老司機都有可能在這個地方翻車裆蒸。在這里,年輕的司機要重新開車啦糖驴,有需要的乘客趕緊刷卡上車僚祷。
筆者之前寫有一篇有關變量提升和作用域的文章,配合食用口味更佳贮缕,本文不再過多贅述作用域有關的知識了辙谜。
執(zhí)行環(huán)境(execution context)及作用域鏈(scope chain)
執(zhí)行環(huán)境(execution context)是 JavaScript 中最為重要的一個概念。注意感昼,沒有之一筷弦。執(zhí)行環(huán)境定義了變量或函數有權訪問的其他數據,決定了它們各自的行為抑诸。每個執(zhí)行環(huán)境都有一個與之關聯的變量對象(variable object)烂琴,環(huán)境中定義的所有變量和函數都保存在這個對象中。我們自己編寫的代碼無法訪問這個對象蜕乡,但是解析器在處理數據時會在后臺使用它奸绷。
全局執(zhí)行環(huán)境
全局執(zhí)行環(huán)境就是最外圍的執(zhí)行環(huán)境。根據 JavaScript 實現所在的宿主環(huán)境的不一樣层玲,表示執(zhí)行環(huán)境的對象也不一樣号醉,在 Web 瀏覽器中,全局的執(zhí)行環(huán)境就是 window 對象辛块,所以我們說畔派,一旦在最外圍聲明了一個變量或者函數,都會自動成為 window 對象下的一個屬性或方法润绵。某個執(zhí)行環(huán)境的代碼要在執(zhí)行完畢之后都應該會被銷毀线椰,其中的所有變量和方法都會被銷毀。但是全局執(zhí)行環(huán)境會等到程序結束(關閉網頁或者瀏覽器)時才會被銷毀尘盼。
執(zhí)行環(huán)境
雖然筆者也很疑惑為什么函數的執(zhí)行環(huán)境不叫局部執(zhí)行環(huán)境或者叫函數執(zhí)行環(huán)境憨愉,只分全局執(zhí)行環(huán)境和函數的執(zhí)行環(huán)境烦绳。那大家還是按照規(guī)范稱呼吧。
每個函數都有自己的執(zhí)行環(huán)境配紫。當執(zhí)行流進入了一個函數時径密,函數的環(huán)境就會被推入一個環(huán)境棧之中。而在函數執(zhí)行后躺孝,棧將環(huán)境彈出享扔,把控制權返回給之前的執(zhí)行環(huán)境。JavaScript 程序中的執(zhí)行流正是由這個機制控制著植袍。這里有一點點像棧方法惧眠,大家可以在腦海里腦補一下棧方法,后進先出奋单。
作用域鏈
當代碼在一個環(huán)境中執(zhí)行的時候,會創(chuàng)建變量對象的一個作用域鏈(scope chain)猫十。作用域鏈的用途览濒,是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數的有序訪問。作用域的前端拖云,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象贷笛。如果這個環(huán)境勢函數,則將其活動對象(activation object)作為變量對象宙项》蹈桑活動對象最開始就只包含一個內部的變量兽泄,即 arguments
對象(注意在全局環(huán)境中不存在這個對象)。而作用域中下一個變量對象來自于包含(外部)環(huán)境,再下一個變量則來自再下一個包含環(huán)境咪橙,一直延伸到全局的執(zhí)行環(huán)境。全局執(zhí)行環(huán)境永遠是作用域鏈中的最后一個對象秩铆。
就好像竖席,假設我們要找一家旅店(變量)下榻,我們最開始肯定是找方圓一百米的旅店(函數的執(zhí)行環(huán)境)油昂,但是一旦方圓一百米內我們沒有找到旅店革娄,那我們只能擴大搜索范圍,去找方圓一公里以內的所有旅店(下一個包含環(huán)境)冕碟,直到找遍整個城市拦惋,最終找到我們所要找的旅店。
話說的再多不如上代碼來的實在:
var first = "first";
function first() {
var second = "second";
function second() {
var third = "third";
//在這可以訪問到 first 安寺、second厕妖、third
}
//在這里可以訪問到 first、second
}
//在這里只能訪問到 first
以上的代碼就有三個環(huán)境挑庶,一個是全局環(huán)境叹放,另外是 first()
局部環(huán)境和 second()
局部環(huán)境饰恕。而它們的訪問權限我已經在注釋中標出來了。內部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境井仰,但是外部環(huán)境不能訪問內部環(huán)境中的任何變量和函數埋嵌。這些環(huán)境之間的聯系是線性、有次序的俱恶。
不過這里面有一個小坑雹嗦,既然說到這,我就順便提一下合是,新手上路估計沒踩過了罪,但是老司機估計已經遇到過了。我們在寫上面的代碼時候聪全,假設我們忘記給變量 third
前面加 var
泊藕,我們都知道,如果函數內變量前如果沒有 var
难礼,那這個變量就會自動變成全局變量娃圆,這個說法并沒有錯,但是有一個情況蛾茉,我們看下面的代碼:
var name = "first";
function first() {
var name = "second";
function second() {
name = "third";
console.log(name);
}
second();
console.log(name);
}
first();
console.log(name);
上面的代碼讼呢,有3個名字叫 name
的變量,分別在三個環(huán)境中谦炬,我們都知道悦屏,變量有作用域,如果是正常的三個同名變量键思,在局部中覆蓋全局的值础爬,正常應該就是輸出 third、second吼鳞、first幕帆,但是我們如果在最里面的 name
中忘了加 var
,按照資料所說赖条,是不是推測輸出的結果是:third失乾、second、third?因為聲明了全局變量呀纬乍?
既然我這么說了當然結果不是這樣碱茁,輸出結果 third、 third仿贬、first 纽竣。什么?全局變量 name
居然沒有改變?當然其實在日常使用中蜓氨,遇到這種情況幾乎不可能聋袋,聲明不加 var
這本來就不符合規(guī)范。那我們就先用不符合規(guī)范的眼光來看待這個情況吧穴吹。
當我們在函數的作用域中聲明變量沒有使用 var
的時候幽勒,會去外部環(huán)境去尋找同名變量,直到找到全局變量港令,如果都找不到這個屬性啥容,就會聲明成全局變量。但是一旦找到同名變量顷霹,就會將值賦給同名變量咪惠。上面的情況也是一樣,當找到了函數 First
發(fā)現有同名變量淋淀,將值賦給 First
中的 name
遥昧,同時就停止查找了,所以并沒有影響到全局變量中的 name
朵纷。
延長作用域鏈
雖然執(zhí)行環(huán)境的類型總共只有兩種——全局和局部(函數)炭臭,但是還是有其他辦法來延長作用域鏈。這么說是因為有些語句可以在作用域鏈的前端臨時增加一個變量對象柴罐,該變量對象會在代碼執(zhí)行后被移除徽缚。具體來說憨奸,就是當執(zhí)行流進入下列任何一個語句時革屠,作用域鏈就會得到加長。
下面兩種情況都會發(fā)生這種現象:
- try-catch 語句的 catch 塊
- with 語句
這兩個語句都會在作用域鏈的前端添加一個變量對象排宰。
- 對
with
語句來說似芝,會將制定的對象添加到作用域鏈中。 - 對
catch
語句來說板甘,會創(chuàng)建一個新的變量對象党瓮,其中包含的是被拋出的錯誤對象的聲明。
這邊不多加贅述了盐类。
閉包
聽說很多老司機總是搞不清匿名函數和閉包這兩個概念寞奸。其實閉包是指有權訪問另一個函數作用域中變量的函數。創(chuàng)建閉包的最簡單的方式在跳,就是在一個函數的內部創(chuàng)建另一個函數枪萄。
不過我們更多時候的形容是:一個函數調用了另一個函數的變量的函數,我們稱為閉包猫妙。畢竟瓷翻,一個函數內部的函數,如果沒有用到外部函數的值,那跟它外部究竟有沒有函數這又有什么關系呢齐帚,都自力更生了妒牙。
閉包與變量
作用域鏈的配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值对妄。
<body>
<button>我是第0個按鈕</button>
<button>我是第1個按鈕</button>
<button>我是第2個按鈕</button>
<script type="text/javascript">
var btns = document.getElementsByTagName("button");
for(var i = 0; i < btns.length; i++){
btns[i].onclick = function() {
alert("我是第" + i + "個按鈕");
}
}
</script>
</body>
這是一個很經典的利用循環(huán)來給按鈕賦值的一個例子湘今。我們的設想是我們點擊按鈕的時候,點擊第一個按鈕提示 “我是第0個按鈕”饥伊,點擊第二個按鈕提示 “我是第1個按鈕”象浑,以此類推。但是事實上琅豆,當我們運行的時候就會發(fā)現愉豺,無論點擊哪個按鈕,都會提示:“我是第3個按鈕”茫因。此時每個函數都引用著保存變量 i
的同一個變量對象蚪拦,所以在每個函數內部 i
的值都是 3 。我們可以通過創(chuàng)建另一個匿名函數強制讓閉包的行為符合預期冻押。
var btns = document.getElementsByTagName("button");
for(var i = 0; i < btns.length; i++){
btns[i].onclick = (function(num){
return function() {
alert("我是第" + num + "個按鈕");
}
})(i);
}
參考文獻:JavaScript 高級程序設計(第三版)