JS入門難點解析9-閉包的深入解析

(注1:如果有問題歡迎留言探討,一起學習!轉載請注明出處,喜歡可以點個贊哦?。?br> (注2:更多內(nèi)容請查看我的目錄。)

1. 簡介

閉包,是讓很多JS初學者聞之色變的一個概念。每次看過一些書籍或者網(wǎng)上的例子,會感覺自己懂了很多,但又是似懂非懂。這篇文章,我們會結合前面所學,深入探討一下閉包的原理,讓大家從根本上弄明白閉包產(chǎn)生的原因。

2. 定義

關于閉包的定義,是讓大家迷惑的第一個點。因為不同書籍,不同的大神對閉包的解讀和定義不盡相同。

2.1. 定義一

我們先來看一下《JavaScript高級程序設計》一書中對閉包的的定義:

閉包是指有權訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)。

再來看一下百度百科(百度百科-閉包)中對閉包的定義:

閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在javascript中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,所以閉包可以理解成“定義在一個函數(shù)內(nèi)部的函數(shù)“。在本質(zhì)上,閉包是將函數(shù)內(nèi)部和函數(shù)外部連接起來的橋梁。

可以看到,百度百科和《JavaScript高級程序設計》一書對閉包的定義基本相同,即“定義在一個函數(shù)內(nèi)部的函數(shù)”。這是目前比較普遍被接受的一個定義。

2.2 定義二

看一下MDN對閉包的定義:

閉包是一個函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合。從理論角度來說,所有函數(shù)都是閉包。

該說法認為“所有函數(shù)都是閉包”,是一個很寬泛的概念。

2.3 我對閉包的理解定義

其實,網(wǎng)上還有許多關于閉包的定義。說法各不相同,有從函數(shù)定義的角度出發(fā),有從使用的角度出發(fā),眾說紛紜,讓人無所適從??墒俏以诖酥涣信e了以上兩種定義,為什么呢?是對其余的定義不認同嗎?并不是。是因為,對于閉包來講,我們關注的并不是其定義或者概念,而是一種現(xiàn)象和產(chǎn)生這種現(xiàn)象的機制原理。那就是一個函數(shù)被嵌套時,不管在哪里被調(diào)用,為什么總能訪問其外層嵌套函數(shù)作用域的變量?這么說,可能有人不理解,我們舉一個簡單的例子:

全局A定義函數(shù)B,函數(shù)B嵌套函數(shù)C,函數(shù)C嵌套函數(shù)D,函數(shù)D在C直接執(zhí)行,或者通過賦值或者返回,在B,A任何一個作用域內(nèi)被引用,當其執(zhí)行時,都可以訪問C,B,A作用域內(nèi)定義的變量。(當然,A作為全局作用域,只要程序未銷毀,其中定義的變量始終是可以被訪問的,所以不做討論。)

所以,非要讓我對閉包下一個定義,我是傾向于定義一的。這其實是分了兩個情況:

  1. 被嵌套函數(shù)只在當前作用域。
  2. 被嵌套函數(shù)在非當前作用域被引用。

對應了兩個使用場景:

  1. 被嵌套函數(shù)只在當前作用域執(zhí)行。
  2. 被嵌套函數(shù)在非當前作用域被執(zhí)行。

下面,我們來分析一下這兩種情況的深層原因。

3. 深入解析閉包

3.1 被嵌套函數(shù)只在當前作用域

對于這種情況,其實在前面文章中我們已經(jīng)做了很詳盡的解釋。之所以被嵌套函數(shù)有權訪問其外層嵌套函數(shù)作用域中的變量,是因為作用域鏈的原因。

看下面這段代碼:

function foo() {
    var a = 2;

    function f() {
        console.log(a); // 2
    }

    f();
}

foo();

為什么在f的內(nèi)部能訪問foo作用域的變量a呢?我們講過,在f執(zhí)行時,創(chuàng)建f的執(zhí)行上下文。此時:

fooContext = {
    AO = {
        arguments: {
            length: 0
        },
        a: 2
    },
    Scope: [AO, globalContext.VO],
    this: undefined
};

fContext = {
    AO = {
        arguments: {
            length: 0
        }
    },
    Scope: [AO, fooContext.AO, globalContext.VO],
    this: undefined
};

此時,執(zhí)行

console.log(a);

會對a進行RHS查找,查找過程是沿著作用域鏈向上,此時在fooContext.AO找到了a的值為2。

3.2 被嵌套函數(shù)在非當前作用域被引用。

發(fā)生這種情況,可能是被嵌套函數(shù)被當做返回值返回,也可能是直接賦值給了外部的變量。我們來看一下這兩種情況。

3.2.1 被嵌套函數(shù)被當做返回值返回

看如下這段代碼:

function foo() {
    var a = 2;

    function f() {
        console.log(a); // 2
    }

    return f;
}

foo()();

此處的關鍵節(jié)點在于foo的執(zhí)行結果。我們在return f;處打一個斷點,拍一張此時的快照:

ECStack = [
    fooContext,
    globalContext
];

fooContext = {
    AO = {
        arguments: {
            length: 0
        },
        a: 2
    },
    Scope: [AO, globalContext.VO],
    this: undefined
};

f.[[scope]] = fooContext.Scope = [fooContext.AO, globalContext.VO];

然后我們在foo()()處打一個斷點,此時foo()已經(jīng)執(zhí)行完畢,返回了f,準備執(zhí)行foo()(),即f()。

ECStack = [
    globalContext
];

f.[[scope]] = fooContext.Scope = [fooContext.AO, globalContext.VO];

此時,fooContext出棧并銷毀。但是,關鍵的一點請注意,雖然foo的執(zhí)行上下文fooContext銷毀了,也不在對其活動對象fooContext.AO有引用了。但是,f.[[scope]]仍然保留有對fooContext.AO的引用,所以,fooContext.AO并沒有被銷毀,仍然存在于內(nèi)存中。然后我們再往下走,執(zhí)行f()

fContext = {
    AO = {
        arguments: {
            length: 0
        }
    },
    Scope: [AO, fooContext.AO, globalContext.VO],
    this: undefined
}

fContext利用f的[[scope]]信息生成了作用域鏈Scope: [AO, fooContext.AO, globalContext.VO]。此時執(zhí)行

console.log(a);

的過程與3.1示例相同。

3.2.2 直接賦值給了外部的變量

看下面代碼:

var bar;
function foo(){
    var a = 2;
    function f(){
        console.log(a); // 2
    }
   bar = f;
}
foo();
bar();

其實這種情況,和3.2.1的執(zhí)行原理是一樣的。都是因為bar中保留有對foo的執(zhí)行環(huán)境的活動對象的引用。具體的分析過程,大家可以自己嘗試一下。

4. 總結

通過這篇文章的分析,我們可以清晰的看到閉包形成的原理,也可以看出閉包為什么可能造成內(nèi)存被大量占用的原因。這里需要注意的是,只要被嵌套函數(shù)被返回或者賦值給了作用域以外的地方,那么在其所有引用執(zhí)行完畢前,都會造成對其包含函數(shù)執(zhí)行環(huán)境的活動對象的持續(xù)引用。

參考

深入理解閉包系列第一篇——到底什么才是閉包
深入理解閉包系列第二篇——從執(zhí)行環(huán)境角度看閉包
JavaScript深入之閉包
百度百科-閉包
BOOK-《JavaScript高級程序設計(第3版)》第7章

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

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