閉包並不是 JavaScript 特有的,大部分高階語言都具有這一能力。
什麼是閉包?
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
這段是 MDN 上對閉包的定義,理解為:一個函式及其周圍封閉詞法環境中的引用構成閉包。可能這句話還是不好理解,看看示例:
function createAction() {
var message = "封閉環境內的變數";
return function() {
console.log(message);
}
}
const showMessage = createAction();
showMessage(); // output: 封閉環境內的變數
這個示例是一個典型的閉包,有這麼幾點需要注意:
-
showMessage
是createAction
執行後從中返回出來的一個函式。 -
createAction
內部是一個封閉的詞法環境,message
作為該封裝環境內的變數,在外面是絕不可能直接訪問。 -
showMessage
在createAction
外部執行,但執行時卻訪問到其內部定義的區域性變數message
(成功輸出)。這是因為showMessage
引用的函式(createAction
內部的匿名函式),在定義時,繫結了其所處詞法環境(createAction
內部)中的引用(message
等)。 - 繫結了內部語法環境的匿名函式被
return
帶到了createAction
封閉環境之外使用,這才能形成閉包。如果是在createAction
內部呼叫,不算是閉包。
好了,我相信 1, 2, 4 都好理解,但是要理解最重要的第 3 點可能有點困難 —— 困難之處在於,這不是程式設計師能決定的,而是由語言特性決定的。所以不要認為是“你”建立了閉包,因為閉包是語言特性,你只是利用了這一特性。
如果語言不支援閉包,類似上面的程式碼,在執行 showMessage
時,就會找不到 message
變數。我特別想去找一個例子,但是很不幸,我所知道的高階語言,只要能在函式/方法內定義函式的,似乎都支援閉包。
把區域性定義的函式“帶”出去
前面我們提到了可以通過 return
把區域性定義的函式帶出去,除此之外有沒有別的辦法?
函式在這裡已經成為“貨”,和其他貨(變數)沒有區別。只要有辦法把變數帶出去,那就有辦法把函式帶出去。比如,使用一個“容器”物件:
function encase(aCase) {
const dog = "狗狗";
const cat = "貓貓";
aCase.show = function () {
console.log(dog, cat);
};
}
const myCase = {};
encase(myCase);
myCase.show(); // output: 貓貓 狗狗
是不是受到了啟發,有沒有聯想到什麼?
模組和閉包
對了,就是 exports 和 module.exports。在 CJS (CommonJS) 定義的模組中,就可以通過 exports.something
逐一帶貨,也可以通過 module.exports = ...
打包帶貨,但不管怎麼樣,exports
就是帶貨的那一個,只是它有可能是原來安排的 exports
也可能是被換成了自己人的 exports
。
ESM (ECMAScript Module) 中使用了 import
和 export
語法,也只不過是換種方法帶貨出去而已,和 return
帶貨差不多,區別只在於 return
只能帶一個(除非打包),export
可以帶一堆。
還要補充的是,不管是 CJS 還是 ESM,模組都是一個封裝環境,其中定義的東西只要不帶出去,外面是訪問不到的。這和網頁尾本預設的全域性環境不同,要注意區別。
如果用程式碼來表示,大概是定義模組的時候以為是這樣:
const var1 = "我是一個頂層變數吧";
function maybeATopFunction() { }
結果在執行環境中,它其實是這樣的(注意:僅示意):
// module factory
function createModule_18abk2(exports, module) {
const var1 = "我是一個頂層變數吧";
function maybeATopFunction() { }
}
// ... 遙遠的生產線上,有這樣的示意程式碼
const module = { exports: {} };
const m18abk2 = createModule_18abk2(module) ?? module;
// 想明白 createModule_18abk2 為什麼會有一個隨機字尾沒?
還是那個函式嗎?
扯遠了,拉回來。思考一個問題:理論上來說,函式是一個靜態程式碼塊,那麼多次呼叫外層函式返回出來的閉包函式,是同一個嗎?
試試:
function create() {
function closure() { }
return closure;
}
const a = create();
const b = create();
console.log(a === b); // false
如果覺得意外,那把 closure()
換種方式定義看會不會好理解一點:
function create() {
closure = function() { }
return closure;
}
如果還不能理解,再看這個:
function create() {
const a = function () { };
const b = function () { };
console.log(a === b); // false
}
能理解了不:每一次 function
都定義了一個新的函式。函式是新的,名字不重要 —— 你能叫小明,別人也能叫小明不是。
所以,總結一下:
閉包是由一個函式以及其定義時所在封閉環境內的各種資源(引用)構成,拿到的每一個閉包都是獨一無二的,因為構成閉包的環境資源不同(不同的區域性環境,定義了不同的區域性變數,傳入了不同的引數等)。
閉包,這回搞明白了!
請關注公眾號邊城客棧⇗
看完了先別走,點個贊 ⇓ 啊,讚賞 ⇘ 就更好啦!