JavaScript 中的作用域鏈(scope chain)和閉包

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 高級程序設計(第三版)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末驰贷,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子洛巢,更是在濱河造成了極大的恐慌括袒,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稿茉,死亡現場離奇詭異锹锰,居然都是意外死亡,警方通過查閱死者的電腦和手機漓库,發(fā)現死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門恃慧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人渺蒿,你說我怎么就攤上這事痢士。” “怎么了茂装?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵怠蹂,是天一觀的道長。 經常有香客問我少态,道長城侧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任况增,我火速辦了婚禮赞庶,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己歧强,他們只是感情好澜薄,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摊册,像睡著了一般肤京。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茅特,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天忘分,我揣著相機與錄音,去河邊找鬼白修。 笑死妒峦,一個胖子當著我的面吹牛,可吹牛的內容都是我干的兵睛。 我是一名探鬼主播肯骇,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼祖很,長吁一口氣:“原來是場噩夢啊……” “哼笛丙!你這毒婦竟也來了?” 一聲冷哼從身側響起假颇,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤笨鸡,失蹤者是張志新(化名)和其女友劉穎傲须,沒想到半個月后例衍,有當地人在樹林里發(fā)現了一具尸體昔期,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡甘改,死狀恐怖腾节,靈堂內的尸體忽然破棺而出禀倔,到底是詐尸還是另有隱情鞋既,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站抵乓,受9級特大地震影響蜈出,放射性物質發(fā)生泄漏。R本人自食惡果不足惜沪哺,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一辜妓、第九天 我趴在偏房一處隱蔽的房頂上張望榴啸。 院中可真熱鬧鸥印,春花似錦库说、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谢谦。三九已至释牺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間回挽,已是汗流浹背没咙。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留千劈,地道東北人祭刚。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像墙牌,于是被迫代替她去往敵國和親涡驮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容