你不知道的JavaScript之閉包

什么是閉包

《JavaScript高級(jí)程序設(shè)計(jì)第三版》:閉包是指有權(quán)訪(fǎng)問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的常見(jiàn)方式,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)。
《你不知道的JavaScript(上卷)》:當(dāng)函數(shù)可以記住并訪(fǎng)問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。

在分析閉包之前,我們首先看段代碼:

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

按照第一種定義,這個(gè)就是閉包了,因?yàn)樵谝粋€(gè)函數(shù)foo內(nèi)部創(chuàng)建另一個(gè)函數(shù)bar()。其實(shí),我們仔細(xì)看下定義就會(huì)發(fā)現(xiàn):在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)是創(chuàng)建閉包的常見(jiàn)方式,并不是閉包的定義。確切的說(shuō),上述代碼中bar() 對(duì)a 的引用的方法是詞法作用域的查找規(guī)則,而這些規(guī)則只是閉包的一部分。

var a = 2;
(function IIFE() {
  console.log( a );//2
})();

這個(gè)是閉包嗎?按照前面的定義,并不是,因?yàn)镮IFE這個(gè)函數(shù)并不是在它本身的詞法作用域以外執(zhí)行的,a 是通過(guò)普通的詞法作用域查找而非閉包被發(fā)現(xiàn)的。

那到底什么是閉包呢,我們繼續(xù)往下看:

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

沒(méi)錯(cuò),在上面例子中,bar()在自己定義的詞法作用域以外的地方被執(zhí)行,這就是閉包。

一般情況下,由于有垃圾回收機(jī)制,在foo() 執(zhí)行后,foo() 的整個(gè)內(nèi)部作用域都被銷(xiāo)毀。而閉包的“神奇”之處在于可以阻止這件事情的發(fā)生。事實(shí)上,bar()在使用foo() 的內(nèi)部作用域,所以這個(gè)內(nèi)部作用域依然存在,拜bar() 所聲明的位置所賜,它擁有涵蓋foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,使得bar() 在之后任何時(shí)間進(jìn)行引用。bar() 對(duì)foo()的作用域的引用,就叫作閉包。

function foo() {
    var a = 2;
    function baz() {
        console.log( a ); // 2
    }
    bar( baz );
}
function bar(fn) {
    fn(); 
}
var fn;
function foo() {
    var a = 2;
    function baz() {
      console.log( a );
    }
    fn = baz; // 將baz 分配給全局變量
}
function bar() {
    fn(); // 這就是閉包!
}
foo();
bar(); // 2

上述兩段代碼的區(qū)別在于,函數(shù)值的傳遞方式不同,但其運(yùn)行結(jié)果一樣,而且都產(chǎn)生了閉包。因此,無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會(huì)持有對(duì)原始定義作用域的引用,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。

我們?cè)賮?lái)分析閉包中經(jīng)典的for循環(huán)問(wèn)題

for(var i=0;i<5;i++){
    setTimeout(function timer(){
        console.log( i );
    },i*1000)
}

如果你認(rèn)為這段代碼的運(yùn)行結(jié)果為分五次輸出0,1,2,3,4,每次間隔為1秒,那就錯(cuò)了。正確的結(jié)果是,五次輸出都為5,那么,這個(gè)5 是從哪里來(lái)的呢?我們發(fā)現(xiàn)這個(gè)循環(huán)的終止條件是i >=5。條件首次成立時(shí)i 的值是5。因此,輸出顯示的是循環(huán)結(jié)束時(shí)i 的最終值。

在這段代碼中我們?cè)噲D假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè)i 的副本。但是根據(jù)作用域的工作原理,當(dāng)某個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)及相應(yīng)的作用域鏈。然后,使用arguments 和其他命名參數(shù)的值來(lái)初始化函數(shù)的活動(dòng)對(duì)象(activation object)。在作用域鏈中,外部函數(shù)的活動(dòng)對(duì)象始終處于第二位,外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位,……直至為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境。

函數(shù)執(zhí)行過(guò)程中,為讀取和寫(xiě)入變量的值,就需要在作用域鏈中查找變量,盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè)i。所以要想搞清楚閉包,首先需要明白作用域鏈的工作原理。

作用域鏈

我們看下面這段代碼:

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);

以上代碼先定義了compare()函數(shù),然后又在全局作用域中調(diào)用了它。當(dāng)調(diào)用compare()時(shí),會(huì)創(chuàng)建一個(gè)包含arguments、value1 和value2 的活動(dòng)對(duì)象。全局執(zhí)行環(huán)境的變量對(duì)象(包含result和compare)在compare()執(zhí)行環(huán)境的作用域鏈中則處于第二位。下圖展示了包含上述關(guān)系的compare()函數(shù)執(zhí)行時(shí)的作用域鏈。

后臺(tái)的每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對(duì)象——變量對(duì)象。全局環(huán)境的變量對(duì)象始終存在,而像compare()函數(shù)這樣的局部環(huán)境的變量對(duì)象,則只在函數(shù)執(zhí)行的過(guò)程中存在。在創(chuàng)建compare()函數(shù)時(shí),會(huì)創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]屬性中。

當(dāng)調(diào)用compare()函數(shù)時(shí),會(huì)為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境,然后通過(guò)復(fù)制函數(shù)的[[Scope]]屬性中的對(duì)象構(gòu)建起執(zhí)行環(huán)境的作用域鏈。此后,又有一個(gè)活動(dòng)對(duì)象(在此作為變量對(duì)象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端。對(duì)于這個(gè)例子中compare()函數(shù)的執(zhí)行環(huán)境而言,其作用域鏈中包含兩個(gè)變量對(duì)象:本地活動(dòng)對(duì)象和全局變量對(duì)象。

顯然,作用域鏈本質(zhì)上是一個(gè)指向變量對(duì)象的指針列表,它只引用但不實(shí)際包含變量對(duì)象。

無(wú)論什么時(shí)候在函數(shù)中訪(fǎng)問(wèn)一個(gè)變量時(shí),就會(huì)從作用域鏈中搜索具有相應(yīng)名字的變量。一般來(lái)講,當(dāng)函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就會(huì)被銷(xiāo)毀,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對(duì)象)。但是,閉包的情況又有所不同。

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}

在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會(huì)將包含函數(shù)(即外部函數(shù))的活動(dòng)對(duì)象添加到它的作用域鏈中。因此,在createComparisonFunction()函數(shù)內(nèi)部定義的匿名函數(shù)的作用域鏈中,實(shí)際上將會(huì)包含外部函數(shù)createComparisonFunction()的活動(dòng)對(duì)象。這段代碼的作用域鏈如下所示

在匿名函數(shù)從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數(shù)的活動(dòng)對(duì)象和全局變量對(duì)象。這樣,匿名函數(shù)就可以訪(fǎng)問(wèn)在createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()函數(shù)在執(zhí)行完畢后,其活動(dòng)對(duì)象也不會(huì)被銷(xiāo)毀,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對(duì)象。換句話(huà)說(shuō),當(dāng)createComparisonFunction()函數(shù)返回后,其執(zhí)行環(huán)境的作用域鏈會(huì)被銷(xiāo)毀,但它的活動(dòng)對(duì)象仍然會(huì)留在內(nèi)存中;直到匿名函數(shù)被銷(xiāo)毀后,createComparisonFunction()的活動(dòng)對(duì)象才會(huì)被銷(xiāo)毀,例如:

//創(chuàng)建函數(shù)
var compareNames = createComparisonFunction("name");
//調(diào)用函數(shù)
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除對(duì)匿名函數(shù)的引用(以便釋放內(nèi)存)
compareNames = null;

首先,創(chuàng)建的比較函數(shù)被保存在變量compareNames 中。而通過(guò)將compareNames 設(shè)置為等于null解除該函數(shù)的引用,就等于通知垃圾回收例程將其清除。隨著匿名函數(shù)的作用域鏈被銷(xiāo)毀,其他作用域(除了全局作用域)也都可以安全地銷(xiāo)毀了。

理解了作用域鏈的銷(xiāo)毀機(jī)制,我們也就清楚了前面的for循環(huán)問(wèn)題中的閉包問(wèn)題,由于變量i存在于全局變量對(duì)象中,每次調(diào)用timer時(shí),都是沿著作用域鏈去查找i的值,我們知道,延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是setTimeout(.., 0),所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此每次輸出5就不難理解了。

整理自《JavaScript高級(jí)程序設(shè)計(jì)第三版》、《你不知道的JavaScript(上卷)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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