JS閉包精講

2020-07-16 10:05:09
閉包是 JavaScript 的重要特性之一,在函數語言程式設計中有著重要的作用,本節介紹閉包的結構和基本用法。

定義閉包

閉包就是一個能夠持續存在的函數上下文活動物件。

形成原理

函數被呼叫時,會產生一個臨時上下文活動物件。它是函數作用域的頂級物件,作用域內所有私有方法有變數、引數、私有函數等都將作為上下文活動物件的屬性而存在。

函數被呼叫後,在預設情況下上下文活動物件會被立即釋放,避免佔用系統資源。但是,若函數內的私有變數、引數、私有函數等被外界參照,則這個上下文活動物件暫時會繼續存在,直到所有外界參照被登出。

但是,函數作用域是封閉的,外界無法存取。那麼在什麼情況下,外界可以存取到函數內的私有成員呢?

根據作用域鏈,內部函數可以存取外部函數的私有成員。如果內部函數參照了外部函數的私有成員,同時內部函數又被傳給外界,或者對外界開放,那麼閉包體就形成了。這個外部函數就是一個閉包體,它被呼叫後,活動物件暫時不會被登出,其屬性會繼續存在,通過內部函數可以持續讀寫外部函數的私有成員。

閉包結構

典型的閉包體是一個巢狀結構的函數。內部函數參照外部函數的私有成員,同時內部函數又被外界參照,當外部函數被呼叫後,就形成了閉包。這個函數也稱為閉包函數。

下面是一個典型的閉包結構。
function f(x) {  //外部函數
    return function (y) {  //內部函數,通過返回內部函數,實現外部參照
        return x + y;  //存取外部函數的引數
    };
}
var c = f(5);  //呼叫外部函數,獲取參照內部函數
console.log(c(6));  //呼叫內部函數,原外部函數的引數繼續存在
解析過程簡單描述如下:
  1. 在 JavaScript 指令碼預編譯期,宣告的函數 f 和變數 c,先被詞法預解析。
  2. 在 JavaScript 執行期,呼叫函數 f,並傳入值 5。
  3. 在解析函數 f 時,將建立執行環境(函數作用域)和活動物件,並把引數和私有變數、內部函數都對映為活動物件的屬性。
  4. 引數 x 的值為 5,對映到活動物件的 x 屬性。
  5. 內部函數通過作用域鏈參照了引數 x,但是還沒有被執行。
  6. 外部函數被呼叫後,返回內部函數,導致內部函數被外界變數 c 參照。
  7. JavaScript 解析器檢測到外部函數的活動物件的屬性被外界參照,無法登出該活動物件,於是在記憶體中繼續維持該物件的存在。
  8. 當呼叫 c,即呼叫內部函數時,可以看到外部函數的引數 x 儲存的值繼續存在。這樣就可以實現後續運算操作,返回 x+y=5=6=11。

如下結構形式也可以形成閉包:通過全域性變數參照內部函數,實現內部函數對外開放。
var c;  //宣告全域性變數
function f(x) {  //外部函數
    c = function (y) {  //內部函數,通過向全域性變數開放實現外部參照
        return x + y;  //存取外部函數的引數
    };
}
f(5);  //呼叫外部函數
console.log(c(6));  //使用全域性變數c呼叫內部函數,返回11

閉包變體

除了巢狀函數外,如果外部參照函數內部的私有陣列或物件,也容易形成閉包。
var add;  //全域性變數
function f() {  //外部函數
    var a = [1,2,3];  //私有變數,參照型陣列
    add = function (x) {  //測試函數,對外開放
        a[0] = x * x;  //修改私有陣列的元素值
    }
    return a;  //返回私有陣列的參照
}
var c = f();
console.log(c[0]);  //讀取閉包內陣列,返回1
add(5);  //測試修改陣列
console.log(c[0]);  //讀取閉包內陣列,返回25
add(10);  //測試修改陣列
console.log(c[0]);  //讀取閉包內陣列,返回100
與函數相同,物件和陣列也是參照型資料。呼叫函數 f,返回私有陣列 a 的參照,即傳值給區域性變數 c,而 a 是函數 f 的私有變數,當被呼叫後,活動物件繼續存在,這樣就形成了閉包。

這種特殊形式的閉包沒有實際應用價值,因為其功能單一,只能作為一個靜態的、單向的閉包。而閉包函數可以設計各種複雜的運算表示式,它是函數式變成的基礎。

反之,如果返回的是一個簡單的值,就無法形成閉包,值傳遞是直接複製。外部變數 c 得到的僅是一個值,而不是對函數內部變數的參照。這樣當函數呼叫後,將直接登出物件。
function f(x) {  //外部函數
    var a = 1;  //私有變數
    return a;
}
var c = f(5);
console.log(c);  //僅是一個值,返回1

使用閉包

下面結合範例介紹閉包的簡單使用,以加深對閉包的理解。

範例1

使用閉包實現優雅的打包,定義記憶體。
var f = function () {  //外部函數
    var a = [];  //私有陣列初始化
    return function (x) {  //返回內部函數
        a.push(x);  //新增元素
        return a;  //返回私有陣列
    };
} ()  //直接呼叫函數,生成執行環境
var a = f(1);  //新增值
console.log(a);  //返回1
var b = f(2);  //新增值
console.log(b);  //返回1,2
在上面範例中,通過外部函數設計一個閉包,定義一個永久的記憶體。當呼叫外部函數生成執行環境之後,就可以利用返回的匿名函數不斷地的向閉包體內的陣列 a 傳入新值,傳入的值會持續存在。

範例2

在網頁中事件處理常式很容易形成閉包。
<script>
function f() {
    var a = 1;
    b = function () {
        console.log("a =" + a);
    }
    c = function () {
        a ++;
    }
    d = function () {
        a --;
    }
}
</script>
<button onclick="f()">生成閉包</button>
<button onclick="b()">檢視 a 的值</button>
<button onclick="c()">遞增</button>
<button onclick="d()">遞減</button>
在瀏覽器中瀏覽時,首先點選“生成閉包”按鈕,生成一個閉包;點選“檢視 a 的值”按鈕,可以隨時檢視閉包內私有變數 a 的值;點選“遞增”“遞減”按鈕時,可以動態修改閉包內變數 a 的值,效果如圖所示。