JavaScript之閉包

翻牆的小哲?發表於2018-05-12

本文一共 1300 字,讀完只需 5 分鐘

概述

閉包, 可以說是每個前端工程師都聽說的一個詞,咋一看很難從字面上去理解,從而給人留下了閉包是一個重要又難以理解的概念。

但是,閉包在 JS 程式碼可以說是隨處可見,閉包也只是計算機領域的一個概念而已,它的存在是因為 JS 的一些語言特性,比如:函式式語言執行上下文執行上下文棧,作用域鏈詞法作用域

執行上下文:Execution Context
執行上下文棧:Execution Context Stack
作用域鏈:Scope Chain
作用域:Scope

本篇文章,將先給結論,到底什麼是閉包,再來分析產生閉包的過程和原因。

一、什麼是閉包

當函式記住並訪問所在詞法作用域的自由變數時,就產生了閉包,即使函式是在當前詞法作用域外執行。 --《你不知道的 JavaScript》

來段經典的閉包程式碼:

function outter() {
    var a = 123;
    
    function inner() {
        console.log(a);
    }
    return inner;
}

var foo = outter();
foo();  // 123
複製程式碼

內部函式 inner 記住了它被定義時的詞法作用域,也就是 outter 的函式作用域,並訪問了該作用域裡的自由變數 a, 同時,inner 函式作用返回值,在外部作用域中被執行。

以上描述,全部符合閉包的描述,那這就是閉包

二、執行過程

之前的文章講了函式的執行上下文棧,變數物件,作用域鏈等內容,接下來通過閉包程式碼回顧程式碼是怎麼樣的執行過程。

function outter() {
    var a = 123;
    
    function inner() {
        console.log(a);
    }
    return inner;
}

var foo = outter();
foo();  // 123
複製程式碼
  1. 進入全域性程式碼的執行上下文,全域性上下文被壓入執行上下文棧。
ECStack = [
        globalContext
    ];
複製程式碼
  1. 全域性上下文建立全域性變數物件,建立 this 並指向全域性上下文。
globalContext = {
    VO: global,
    scope: [global.VO],
    this: global
}
複製程式碼
  1. 全域性上下文初始化時,outter 函式被建立,建立作用域鏈,複製 Scope 屬性到 outter 函式的內部屬性[[scope]]
  outter.[[scope]] = [     
    globalContext.VO
  ];
複製程式碼
  1. 執行 outter 函式,建立 outter 函式執行上下文,將 outter 上下文壓入執行上下文棧。
ECStack = [
        globalContext,
        outterContext
    ];
複製程式碼
  1. 初始化 outter 函式執行上下文,用 arguments 建立活動物件,加入形參、函式宣告、變數宣告。將活動物件壓入 outter 作用域鏈頂端。
outterContext = {
    AO: {
        arguments: {
            a: undefined,
        }
        length: 1
    },
    scope: undefined,
    inner: reference to function inner(){}
    Scope: [AO, globalContext.VO],
    this: undefined
}
複製程式碼
  1. outter 執行完畢,接著執行 outter 返回的被變數引用的函式 inner;
ECStack = [
        globalContext,
        innerContext
    ];
複製程式碼
  1. inner 函式初始化,過程和第4步一樣。
innerContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, outterContext.AO, globalContext.VO],
        this: undefined
    }
複製程式碼
  1. inner 執行,沿著作用域鏈查詢變數 a, 列印 a 值。
  2. inner 函式執行結束,彈出執行上下文棧。
ECStack = [
        globalContext
    ];
複製程式碼

在這個過程中,第 5 步,outter 已經執行結束,執行上下文按理來說已經被銷燬,內部函式 inner 怎麼還能訪問 outter 作用域的變數呢。

正是由於閉包,inner 引用了它所在詞法作用域的自由變數 a,inner 的作用域鏈中仍然是完整的, 儘管 inner 在其他地方執行,還是返回了正確結果。

三、函式式語言

閉包中,一個很重要的特點就是,內部函式作為一個資料被返回。這是由於 JS 是函式式語言,函式可以作為引數傳遞進函式,也可以作為一個資料返回。函式的巢狀構成了作用域的巢狀,也就有了作用域鏈。

由於函式具有作用域,且變數的尋找具有 “遮蔽效應”(從內到外,找到第一個就停止),使得區域性作用域的變數對於外部作用域是不可見的,於是函式就有了封閉性,所以我們拿函式來包裹封裝私有變數,同時也有了閉包。

四、自由變數

自由變數是指在函式中使用的,但既不是函式引數也不是函式的區域性變數的變數。

function outter() {
    var a = 123;
    
    function inner() {
        console.log(a);
    }
    return inner;
}

var foo = outter();
foo();  // 123
複製程式碼

對於 inner 函式而言,變數 a, 不是它的函式引數,也不是它的區域性變數,a 就是自由變數。

五、閉包的用處和缺點

從閉包的特點可以看出,自由變數儲存在了記憶體中,並能間接訪問。

那麼閉包的作用就是:

隱藏私有變數,解決變數名稱空間汙染的問題。

缺點
如果閉包過多,變數常駐記憶體,肯定會佔用大量記憶體空間。

總結

由於 JS 是函式式語言,當函式記住並訪問所在詞法作用域的自由變數時,就產生了閉包,即使函式是在當前詞法作用域外執行。

閉包在 JS 程式碼中非常常見,不必把它想得太玄乎。

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JavaScript之閉包

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply

相關文章