7.2 閉包

有不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數作用域中得變量的函數創建閉包的常見方式,就是在一個函數內部創建另一個函數,仍以前面的 createComparisonFunction() 函數爲例,注意加粗的代碼。

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

在這個例子中,突出的那兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部函數中的變量 prototypeName。即使這個內部函數被返回了,而且是在其他地方被調用了,但它仍然可以訪問變量 prototypeName。之所以能夠訪問這個變量,是因爲內部函數的作用域鏈中包含 createComparisonFunction()的作用域。要徹底搞清楚其中的細節,必須從理解函數被調用的時候都會發生什麼入手。

第四章介紹了作用域鏈的概念。而有關如何創建作用域鏈以及作用域鏈有什麼作用的細節,對徹底理解閉包至關重要。當某個函數被調用時,會創建一個執行環境(execution context)及相應的作用域鏈。然後,使用 arguments和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,…直至作爲作用域鏈終點的全局執行環境。

在函數執行過程中,爲讀取和寫入變量的值,就需要在作用域鏈中查找變量。來看下面的例子。

function compare(value1,value2) {
    if(value1<value2){
        return -1;
    }else if(value1>value2){
        return 1;
    }else{
        return 0;
    }
}

var result = compare(5,10);
console.log(result);//-1

以上代碼先定義了 **compare()**函數,然後又在全局作用域中調用了它。當調用 **compare()**時,會創建一個包含 argumentsvalue1value2的活動對象。全局執行環境的變量對象(包含result和compare)在 **compare()**執行環境的作用域鏈中則處於第二位。圖7-1展示了包含上述關係的compare()函數執行時的作用域鏈。
在這裏插入圖片描述

後臺的每個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像 **compare()**函數這樣的局部環境的變量對象,則只在函數執行的過程中存在。在創建 **compare()**函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的 [[Scope]] 屬性中。當調用 **compare()**函數時,會爲函數創建一個執行環境,然後通過複製函數的 [[Scope]] 屬性中的對象構建起執行環境的作用域鏈。此後,又有一個活動對象(在此作爲變量對象使用)被創建並被推入執行環境作用域鏈的前端。對於這個例子中 compare() 函數的執行環境而言,其作用域鏈中包含兩個變量對象本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
無論什麼時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局作用域(全局執行環境的變量對象)但是,閉包的情況又有所不同
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在 createComparisonFunction() 函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數 createComparisonFunction() 的活動對象圖7-2展示了當下列代碼執行時,包含函數與內部匿名函數的作用域鏈。

var compare = createComparisionFunction("name");
var result = compare({name:"Nicholas"},{name:"Greg"});    

在匿名函數從 **createComparisionFunction()**中被返回後,它的作用域鏈被初始化爲包含 createComparisionFunction() 函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問在 createComparisionFunction() 中定義的所有變量。更爲重要的是,createComparisionFunction() 函數在執行完畢後,其活動對象也不會被銷燬因爲匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當 **createComparisionFunction()**函數返回後,其執行環境的作用域鏈會被銷燬,但它的活動對象仍然會留在內存中;直到匿名函數被銷燬後,**createComparisionFunction()**的活動對象纔會被銷燬,例如:

//創建函數
var compareNames = createComparisionFunction("name");

//調用函數
var result = compareNames({name:"Nicholas"},{name:"Greg"});

//解除對匿名函數的引用(以便釋放內存)
compareNames = null;

首先,創建的比較函數被保存在變量 compareNames 中。而通過將 compareNames設置爲等於 null 解除該函數的引用,就等於通知垃圾回收例程將其清除。隨着匿名函數的作用域鏈被銷燬,其他作用域(除了全局作用域)也可以安全地銷燬了。圖7-2展示了調用compareNames()的過程中產生的作用域鏈之間的關係。
在這裏插入圖片描述
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的內存。過度使用閉包可能會導致內存佔用過多,我們建議讀者只在絕對必要時再考慮使用閉包。雖然像V8等優化後的JavaScript引擎會嘗試收回被閉包占用的內存,但請大家還是要慎重使用閉包。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章