理解Javascript的閉包

tony0087發表於2021-09-09

Javascript中有幾個非常重要的語言特性——物件、原型繼承、閉包。其中閉包對於那些使用傳統靜態語言C/C++的程式設計師來說是一個新的語言特性。本文將以例子入手來介紹Javascript閉包的語言特性,並結合一點ECMAScript語言規範來使讀者可以更深入的理解閉包。

什麼是閉包

閉包是什麼?閉包是Closure,這是靜態語言所不具有的一個新特性。但是閉包也不是什麼複雜到不可理解的東西,簡而言之,閉包就是:

  • 閉包就是函式的區域性變數集合,只是這些區域性變數在函式返回後會繼續存在。
  • 閉包就是就是函式的“堆疊”在函式返回後並不釋放,我們也可以理解為這些函式堆疊並不在棧上分配而是在堆上分配
  • 當在一個函式內定義另外一個函式就會產生閉包

上面的第二定義是第一個補充說明,抽取第一個定義的主謂賓——閉包是函式的‘區域性變數’集合。只是這個區域性變數是可以在函式返回後被訪問。(這個不是官方定義,但是這個定義應該更有利於你理解閉包)

做為區域性變數都可以被函式內的程式碼訪問,這個和靜態語言是沒有差別。閉包的差別在於區域性變變數可以在函式執行結束後仍然被函式外的程式碼訪問。這意味著函式必須返回一個指向閉包的“引用”,或將這個”引用”賦值給某個外部變數,才能保證閉包中區域性變數被外部程式碼訪問。當然包含這個引用的實體應該是一個物件,因為在Javascript中除了基本型別剩下的就都是物件了。可惜的是,ECMAScript並沒有提供相關的成員和方法來訪問閉包中的區域性變數。但是在ECMAScript中,函式物件中定義的內部函式(inner function)是可以直接訪問外部函式的區域性變數,通過這種機制,我們就可以以如下的方式完成對閉包的訪問了。

function greeting(name) { 
    var text = 'Hello ' + name; // local variable 
    // 每次呼叫時,產生閉包,並返回內部函式物件給呼叫者 
    return function() { alert(text); } 
} 
var sayHello=greeting("Closure"); 
sayHello()  // 通過閉包訪問到了區域性變數text 

上述程式碼的執行結果是:Hello Closure,因為sayHello()函式在greeting函式執行完畢後,仍然可以訪問到了定義在其之內的區域性變數text。

好了,這個就是傳說中閉包的效果,閉包在Javascript中有多種應用場景和模式,比如Singleton,Power Constructor等這些Javascript模式都離不開對閉包的使用。

ECMAScript閉包模型

ECMAScript到底是如何實現閉包的呢?想深入瞭解的親們可以獲取ECMAScript 規範進行研究,我這裡也只做一個簡單的講解,內容也是來自於網路。

在ECMAscript的指令碼的函式執行時,每個函式關聯都有一個執行上下文場景(Execution Context) ,這個執行上下文場景中包含三個部分

  • 文法環境(The LexicalEnvironment)
  • 變數環境(The VariableEnvironment)
  • this繫結

其中第三點this繫結與閉包無關,不在本文中討論。文法環境中用於解析函式執行過程使用到的變數識別符號。我們可以將文法環境想象成一個物件,該物件包含了兩個重要元件,環境記錄(Enviroment Recode),和外部引用(指標)。環境記錄包含包含了函式內部宣告的區域性變數和引數變數,外部引用指向了外部函式物件的上下文執行場景。全域性的上下文場景中此引用值為NULL。這樣的資料結構就構成了一個單向的連結串列,每個引用都指向外層的上下文場景。

例如上面我們例子的閉包模型應該是這樣,sayHello函式在最下層,上層是函式greeting,最外層是全域性場景。如下圖:

因此當sayHello被呼叫的時候,sayHello會通過上下文場景找到區域性變數text的值,因此在螢幕的對話方塊中顯示出”Hello Closure”
變數環境(The VariableEnvironment)和文法環境的作用基本相似,具體的區別請參看ECMAScript的規範文件。

閉包的樣列

前面的我大致瞭解了Javascript閉包是什麼,閉包在Javascript是怎麼實現的。下面我們通過針對一些例子來幫助大家更加深入的理解閉包,下面共有5個樣例,例子來自於JavaScript Closures For Dummies(映象)
例子1:閉包中區域性變數是引用而非拷貝

function say667() { 
    // Local variable that ends up within closure 
    var num = 666; 
    var sayAlert = function() { alert(num); } 
    num++; 
    return sayAlert; 
} 
 
var sayAlert = say667(); 
sayAlert() 

因此執行結果應該彈出的667而非666。

例子2:多個函式繫結同一個閉包,因為他們定義在同一個函式內。

function setupSomeGlobals() { 
    // Local variable that ends up within closure 
    var num = 666; 
    // Store some references to functions as global variables 
    gAlertNumber = function() { alert(num); } 
    gIncreaseNumber = function() { num++; } 
    gSetNumber = function(x) { num = x; } 
} 
setupSomeGlobals(); // 為三個全域性變數賦值 
gAlertNumber(); //666 
gIncreaseNumber(); 
gAlertNumber(); // 667 
gSetNumber(12);// 
gAlertNumber();//12 

例子3:當在一個迴圈中賦值函式時,這些函式將繫結同樣的閉包

function buildList(list) { 
    var result = []; 
    for (var i = 0; i < list.length; i++) { 
        var item = 'item' + list[i]; 
        result.push( function() {alert(item + ' ' + list[i])} ); 
    } 
    return result; 
} 
 
function testList() { 
    var fnlist = buildList([1,2,3]); 
    // using j only to help prevent confusion - could use i 
    for (var j = 0; j < fnlist.length; j++) { 
        fnlist[j](); 
    } 
} 

testList的執行結果是彈出item3 undefined視窗三次,因為這三個函式繫結了同一個閉包,而且item的值為最後計算的結果,但是當i跳出迴圈時i值為4,所以list[4]的結果為undefined.

例子4:外部函式所有區域性變數都在閉包內,即使這個變數宣告在內部函式定義之後。

function sayAlice() { 
    var sayAlert = function() { alert(alice); } 
    // Local variable that ends up within closure 
    var alice = 'Hello Alice'; 
    return sayAlert; 
} 
var helloAlice=sayAlice(); 
helloAlice(); 

執行結果是彈出”Hello Alice”的視窗。即使區域性變數宣告在函式sayAlert之後,區域性變數仍然可以被訪問到。

例子5:每次函式呼叫的時候建立一個新的閉包

function newClosure(someNum, someRef) { 
    // Local variables that end up within closure 
    var num = someNum; 
    var anArray = [1,2,3]; 
    var ref = someRef; 
    return function(x) { 
        num += x; 
        anArray.push(num); 
        alert('num: ' + num + 
        '\nanArray ' + anArray.toString() + 
        '\nref.someVar ' + ref.someVar); 
    } 
} 
closure1=newClosure(40,{someVar:'closure 1'}); 
closure2=newClosure(1000,{someVar:'closure 2'}); 
 
closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1' 
closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2' 
 

閉包的應用

Singleton 單件:

var singleton = function () { 
    var privateVariable; 
    function privateFunction(x) { 
        ...privateVariable... 
    } 
 
    return { 
        firstMethod: function (a, b) { 
            ...privateVariable... 
        }, 
        secondMethod: function (c) { 
            ...privateFunction()... 
        } 
    }; 
}(); 

這個單件通過閉包來實現。通過閉包完成了私有的成員和方法的封裝。匿名主函式返回一個物件。物件包含了兩個方法,方法1可以方法私有變數,方法2訪問內部私有函式。需要注意的地方是匿名主函式結束的地方的’()’,如果沒有這個’()’就不能產生單件。因為匿名函式只能返回了唯一的物件,而且不能被其他地方呼叫。這個就是利用閉包產生單件的方法。

參考:

JavaScript Closures For Dummies(映象) 可惜都被牆了。
Advance Javascript (Douglas Crockford 大神的視訊,一定要看啊)

來源:http://coolshell.cn/articles/6731.html

相關文章