簡單而清楚地理解閉包

Lycop發表於2019-02-16

什麼是閉包?
“閉包是指有權訪問另一個函式作用域中的變數的函式。”—《JavaScript高階程式設計》
通常來說,當一個函式可以訪問另一個函式內部定義的變數(包括屬性和方法)時,這個函式可以稱之為閉包:

function fnA(){
    var a = "this is fnA.a";
    return function fnB(){
        alert(a);
    }
}

var x = fnA();
x(); // "this is fnA.a"

例子中,我們可以通過x(即fnB)去訪問fnA中的內部變數(a),此時我們可以稱fnB為閉包。

閉包是如何產生的?
為了更清楚的解釋閉包的發生,我們需要先明白“函式的建立”到“函式的呼叫”到底發生了什麼事情。

1、函式被建立時,會建立一條作用域鏈(下稱A鏈)。然後根據跟建立時的環境,依照“外部函式”、“‘外部函式’的外部函式”、“‘外部函式的外部函式’的外部函式”….“全域性函式”順序,將所有函式的活動物件(可以簡單理解為所有的內部變數)新增到這條作用域鏈上。(大多數非閉包的情況下,函式的外部函式即全域性變數)
2、函式被呼叫時,也會建立一條作用域鏈(下稱B鏈),並將A鏈的內容包含到B鏈中,然後將當前函式的活動物件(可以簡單理解為所有的內部變數)新增到B鏈條的頂端。
3、當訪問函式內部變數時,會按照B鏈中的變數儲存的順序依次訪問。即內部變數,(建立時的)外部函式的變數,(建立時的)外部函式的外部函式的變數…全域性變數。

下面是一道經典的閉包題:

function fun(n,o) {
    console.log(o)
    return {
    fun:function(m){
        return fun(m,n);
        }
    };
}

var a = fun(0); // undefined。由於會“o”未賦值,所以會顯示:undefined。同時返回一個字面量物件,物件內建立一個名為“fun”的函式,並將物件返回賦值給全域性變數a。此時a內部的函式fun已經被建立好了,它的作用域鏈上包含了外部函式(外層的fun函式)的所有變數,其中包含了n(值為0),o(值為undefined);以及全域性函式的變數fun(值得注意的是,這個fun屬於全域性函式的變數)。
a.fun(1); // 0。上面提到。在建立a的內部fun時,它包含的作用域鏈中包含了n(值為0),o(值為undefined);以及全域性函式的變數fun。因此,我們呼叫(訪問)的“fun”是作用域鏈中給全域性函式的函式fun。m=1,n=0,將其賦值給全域性函式的函式fun,即:n=(m=)1,o=(n=)0,列印0,值為“0”。
a.fun(2); // 0
a.fun(3); // 0。這裡有個“坑”需要注意。在上個步驟“a.fun(1);”中,最後會建立一個物件(fun函式作用域鏈中的n值為1,o值為0)並返回。但是並沒有變數來接收這個物件,更不會影響到a內部作用域鏈。因此“a.fun(2);”、“a.fun(3);”中,作用域鏈上的值與“a.fun(1);”中完全一樣。


var b = fun(0).fun(1).fun(2).fun(3); // undefined,0,1,2
//這是一條鏈式呼叫。為了便於理解,我們將鏈式呼叫拆分以下等價的方案:
var b1 = fun(0); // undefined。這個和“ var a = fun(0);”,不重複解釋。
var b2 = b1.fun(1); // 0。這裡和“a.fun(1);”一樣,不重複解釋。但是要注意的是,此時有個變數b2接收了b1.fun返回的變數。此時,b2中的函式fun的作用域鏈的(部分)內容情況:n=1,o=0。
var b3 = b2.fun(2); // 1。“var b2 = b1.fun(1);”中,b2中函式fun的作用域鏈中的n為1,o為0。呼叫全域性函式的fun時,n=(m=)2,o=(n=)1。因此列印內容為“1”。
var b4 = b3.fun(3); // 2。理由同上。


var c = fun(0).fun(1); // undefined,0
c.fun(2);// 1
c.fun(3);// 1

//為了便於理解,我們將鏈式呼叫拆分以下等價的方案進行解釋:
var c1 = fun(0); // undefined。這個和“ var a = fun(0);”,不重複解釋。
var c = c1.fun(1); // 0。要注意的是,“ c1.fun(1); ”返回的物件由變數c接收,即c中的函式fun作用域鏈中的變數:n=1,o=0。
c.fun(2);// 1。
c.fun(3);// 1。“ c.fun(2);”中返回的物件不會影響到c。因此此處和執行“c.fun(2);”時一樣,c中的函式fun作用域鏈並未被改變。

我們可以簡單理解為:函式建立時,就已經根據上下文環境儲存一套所有外部函式(不包含自身內部)的變數。當我們在呼叫閉包函式時,閉包函式自身不存在的變數,將會在這套變數中查詢。

值得一提
1、“變數宣告提升”對於閉包的實現是非常重要的。如果變數宣告沒有被提升,那麼我們將無法儲存那些在閉包函式建立以後才宣告的變數。
2、閉包的機制,作用域鏈會一直引用自身以外的函式的全部變數,記憶體回收機制不能及時回收這些變數,從而增大記憶體開銷。

相關文章