還搞不懂閉包算我輸(JS 示例)

閉包並非 JavaScript 特有的,大部分高級語言都具備這一能力。程序員

什麼是閉包?

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). 閉包

這段是 MDN 上對閉包的定義,理解爲:一個函數及其周圍封閉詞法環境中的引用構成閉包。可能這句話仍是很差理解,看看示例:ide

function createAction() {
    var message = "封閉環境內的變量";

    return function() {
        console.log(message);
    }
}

const showMessage = createAction();
showMessage();  // output: 封閉環境內的變量

這個示例是一個典型的閉包,有這麼幾點須要注意:函數

  1. showMessagecreateAction 執行後從中返回出來的一個函數
  2. createAction 內部是一個封閉的詞法環境,message 做爲該封裝環境內的變量,在外面是毫不可能直接訪問。
  3. showMessagecreateAction 外部執行,但執行時卻訪問到其內部定義的局部變量 message(成功輸出)。這是由於 showMessage 引用的函數(createAction 內部的匿名函數),在定義時,綁定了其所處詞法環境(createAction 內部)中的引用(message 等)。
  4. 綁定了內部語法環境的匿名函數被 return 帶到了 createAction 封閉環境以外使用,這才能造成閉包。若是是在 createAction 內部調用,不算是閉包。

好了,我相信 1, 2, 4 都好理解,可是要理解最重要的第 3 點可能有點困難 —— 困難之處在於,這不是程序員能決定的,而是由語言特性決定的。因此不要認爲是「你」建立了閉包,由於閉包是語言特性,你只是利用了這一特性code

若是語言不支持閉包,相似上面的代碼,在執行 showMessage 時,就會找不到 message 變量。我特別想去找一個例子,可是很不幸,我所知道的高級語言,只要能在函數/方法內定義函數的,彷佛都支持閉包。對象

把局部定義的函數「帶」出去

前面咱們提到了能夠經過 return 把局部定義的函數帶出去,除此以外有沒有別的辦法?ip

函數在這裏已經成爲「貨」,和其餘貨(變量)沒有區別。只要有辦法把變量帶出去,那就有辦法把函數帶出去。好比,使用一個「容器」對象:資源

function encase(aCase) {
    const dog = "狗狗";
    const cat = "貓貓";
    aCase.show = function () {
        console.log(dog, cat);
    };
}

const myCase = {};
encase(myCase);
myCase.show();      // output: 貓貓 狗狗

是否是受到了啓發,有沒有聯想到什麼?get

模塊和閉包

對了,就是 exports 和 module.exports。在 CJS (CommonJS) 定義的模塊中,就能夠經過 exports.something 逐一帶貨,也能夠經過 module.exports = ... 打包帶貨,但無論怎麼樣,exports 就是帶貨的那一個,只是它有多是原來安排的 exports 也多是被換成了本身人的 exportsit

ESM (ECMAScript Module) 中使用了 importexport 語法,也只不過是換種方法帶貨出去而已,和 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 都定義了一個新的函數。函數是新的,名字不重要 —— 你能叫小明,別人也能叫小明不是。

因此,總結一下:


閉包是由一個函數以及其定義時所在封閉環境內的各類資源(引用)構成,拿到的每個閉包都是獨一無二的,由於構成閉包的環境資源不一樣(不一樣的局部環境,定義了不一樣的局部變量,傳入了不一樣的參數等)。

閉包,這回搞明白了!


喜歡此文,點個贊 ⇙

支持做者,賞個咖啡豆 ⇓

相關文章
相關標籤/搜索