JavaScript深入之閉包

JavaScript深入系列第八篇荠锭,介紹理論上的閉包和實踐上的閉包配乱,以及從作用域鏈的角度解析經(jīng)典的閉包題排抬。

定義

MDN 對閉包的定義為:

閉包是指那些能夠訪問自由變量的函數(shù)两踏。

那什么是自由變量呢司草?

自由變量是指在函數(shù)中使用的艰垂,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量。

由此埋虹,我們可以看出閉包共有兩部分組成:

閉包 = 函數(shù) + 函數(shù)能夠訪問的自由變量

舉個例子:

var a = 1;

function foo() {
    console.log(a);
}

foo();

foo 函數(shù)可以訪問變量 a猜憎,但是 a 既不是 foo 函數(shù)的局部變量,也不是 foo 函數(shù)的參數(shù)搔课,所以 a 就是自由變量胰柑。

那么,函數(shù) foo + foo 函數(shù)訪問的自由變量 a 不就是構(gòu)成了一個閉包嘛……

還真是這樣的爬泥!

所以在《JavaScript權(quán)威指南》中就講到:從技術(shù)的角度講柬讨,所有的JavaScript函數(shù)都是閉包。

咦袍啡,這怎么跟我們平時看到的講到的閉包不一樣呢2裙佟?

別著急境输,這是理論上的閉包蔗牡,其實還有一個實踐角度上的閉包,讓我們看看湯姆大叔翻譯的關(guān)于閉包的文章中的定義:

ECMAScript中嗅剖,閉包指的是:

  1. 從理論角度:所有的函數(shù)辩越。因為它們都在創(chuàng)建的時候就將上層上下文的數(shù)據(jù)保存起來了。哪怕是簡單的全局變量也是如此窗悯,因為函數(shù)中訪問全局變量就相當于是在訪問自由變量区匣,這個時候使用最外層的作用域。
  2. 從實踐角度:以下函數(shù)才算是閉包:
    1. 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如亏钩,內(nèi)部函數(shù)從父函數(shù)中返回)
    2. 在代碼中引用了自由變量

接下來就來講講實踐上的閉包莲绰。

分析

讓我們先寫個例子,例子依然是來自《JavaScript權(quán)威指南》姑丑,稍微做點改動:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

首先我們要分析一下這段代碼中執(zhí)行上下文棧和執(zhí)行上下文的變化情況蛤签。

另一個與這段代碼相似的例子,在《JavaScript深入之執(zhí)行上下文》中有著非常詳細的分析栅哀。如果看不懂以下的執(zhí)行過程诵闭,建議先閱讀這篇文章层释。

這里直接給出簡要的執(zhí)行過程:

  1. 進入全局代碼,創(chuàng)建全局執(zhí)行上下文,全局執(zhí)行上下文壓入執(zhí)行上下文棧
  2. 全局執(zhí)行上下文初始化
  3. 執(zhí)行 checkscope 函數(shù)贸宏,創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文渴庆,checkscope 執(zhí)行上下文被壓入執(zhí)行上下文棧
  4. checkscope 執(zhí)行上下文初始化裳涛,創(chuàng)建變量對象咐吼、作用域鏈、this等
  5. checkscope 函數(shù)執(zhí)行完畢咳蔚,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出
  6. 執(zhí)行 f 函數(shù)豪嚎,創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 執(zhí)行上下文被壓入執(zhí)行上下文棧
  7. f 執(zhí)行上下文初始化谈火,創(chuàng)建變量對象侈询、作用域鏈、this等
  8. f 函數(shù)執(zhí)行完畢糯耍,f 函數(shù)上下文從執(zhí)行上下文棧中彈出

了解到這個過程扔字,我們應該思考一個問題,那就是:

當 f 函數(shù)執(zhí)行的時候谍肤,checkscope 函數(shù)上下文已經(jīng)被銷毀了啊(即從執(zhí)行上下文棧中被彈出)啦租,怎么還會讀取到 checkscope 作用域下的 scope 值呢?

以上的代碼荒揣,要是轉(zhuǎn)換成 PHP篷角,就會報錯,因為在 PHP 中系任,f 函數(shù)只能讀取到自己作用域和全局作用域里的值恳蹲,所以讀不到 checkscope 下的 scope 值。(這段我問的PHP同事……)

然而 JavaScript 卻是可以的俩滥!

當我們了解了具體的執(zhí)行過程后嘉蕾,我們知道 f 執(zhí)行上下文維護了一個作用域鏈:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

對的,就是因為這個作用域鏈霜旧,f 函數(shù)依然可以讀取到 checkscopeContext.AO 的值错忱,說明當 f 函數(shù)引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷毀了,但是 JavaScript 依然會讓 checkscopeContext.AO 活在內(nèi)存中以清,f 函數(shù)依然可以通過 f 函數(shù)的作用域鏈找到它儿普,正是因為 JavaScript 做到了這一點,從而實現(xiàn)了閉包這個概念掷倔。

所以眉孩,讓我們再看一遍實踐角度上閉包的定義:

  1. 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如勒葱,內(nèi)部函數(shù)從父函數(shù)中返回)
  2. 在代碼中引用了自由變量

在這里再補充一個《JavaScript權(quán)威指南》英文原版對閉包的定義:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

閉包在計算機科學中也只是一個普通的概念浪汪,大家不要去想得太復雜。

必刷題

接下來凛虽,看這道刷題必刷死遭,面試必考的閉包題:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,讓我們分析一下原因:

當執(zhí)行到 data[0] 函數(shù)之前涩维,此時全局上下文的 VO 為:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

當執(zhí)行 data[0] 函數(shù)的時候殃姓,data[0] 函數(shù)的作用域鏈為:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并沒有 i 值,所以會從 globalContext.VO 中查找瓦阐,i 為 3,所以打印的結(jié)果就是 3篷牌。

data[1] 和 data[2] 是一樣的道理睡蟋。

所以讓我們改成閉包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

當執(zhí)行到 data[0] 函數(shù)之前,此時全局上下文的 VO 為:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟沒改之前一模一樣枷颊。

當執(zhí)行 data[0] 函數(shù)的時候戳杀,data[0] 函數(shù)的作用域鏈發(fā)生了改變:

data[0]Context = {
    Scope: [AO, 匿名函數(shù)Context.AO globalContext.VO]
}

匿名函數(shù)執(zhí)行上下文的 AO 為:

匿名函數(shù)Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并沒有 i 值,所以會沿著作用域鏈從匿名函數(shù) Context.AO 中查找夭苗,這時候就會找 i 為 0信卡,找到了就不會往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值為3)题造,所以打印的結(jié)果就是 0傍菇。

data[1] 和 data[2] 是一樣的道理。

作者:冴羽
github:https://github.com/mqyqingfeng/Blog
掘金主頁:https://juejin.im/user/58e4b9b261ff4b006b3227f4
segmentfault主頁:https://segmentfault.com/u/yayu/articles

Vicky丶Amor 經(jīng)授權(quán)轉(zhuǎn)載界赔,版權(quán)歸原作者所有丢习。
求關(guān)注,求點贊

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淮悼,一起剝皮案震驚了整個濱河市咐低,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袜腥,老刑警劉巖见擦,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡鲤屡,警方通過查閱死者的電腦和手機儡湾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來执俩,“玉大人徐钠,你說我怎么就攤上這事∫凼祝” “怎么了尝丐?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長衡奥。 經(jīng)常有香客問我爹袁,道長,這世上最難降的妖魔是什么矮固? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任失息,我火速辦了婚禮,結(jié)果婚禮上档址,老公的妹妹穿的比我還像新娘盹兢。我一直安慰自己,他們只是感情好守伸,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布绎秒。 她就那樣靜靜地躺著,像睡著了一般尼摹。 火紅的嫁衣襯著肌膚如雪见芹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天蠢涝,我揣著相機與錄音玄呛,去河邊找鬼。 笑死和二,一個胖子當著我的面吹牛徘铝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播儿咱,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼庭砍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了混埠?” 一聲冷哼從身側(cè)響起怠缸,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钳宪,沒想到半個月后揭北,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扳炬,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年搔体,在試婚紗的時候發(fā)現(xiàn)自己被綠了恨樟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡疚俱,死狀恐怖劝术,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呆奕,我是刑警寧澤养晋,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站梁钾,受9級特大地震影響绳泉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姆泻,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一零酪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拇勃,春花似錦四苇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至峻呛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辜窑,已是汗流浹背钩述。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留穆碎,地道東北人牙勘。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像所禀,于是被迫代替她去往敵國和親方面。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容