[譯]理解JS中的閉包

理解JS中的閉包

寫在正文前

此篇文章翻譯自Sukhjinder Arora文章Understanding Closures in JavaScript. 這篇文章結合了閉包,詞法做用域,調用棧以及執行上下文來理解閉包。文章若有翻譯很差的地方還望多多包涵。javascript

理解JS中的閉包

閉包是每個js開發者都須要知道和理解的概念。然而,它也是一個困擾着全部小萌新的概念。java

若是對於閉包有正確理解的話,他會幫助你寫出更好更快更強的代碼。那也就是說,它會幫助你成爲一個更好js開發者。git

所以在這個文章內,我將會嘗試解釋閉包的內部原理,以及他們是如何在實際js中運行的。github

屁話很少說,咱們開始吧:)bash

(廣告時間) 小貼士:當寫了可複用的js代碼的時候,你可能想要不只僅在一個項目中使用他們. Bit是一個很是有用對對小工具方便你快速的分享和整理你的可複用代碼,數據結構

執行上下文

執行上下文是js代碼賦值和執行的抽象環境。當全局代碼執行的時候,他就執行在全局執行上下文內部。函數代碼執行在函數執行上下文內部。閉包

js中有且只有一個當前正在運行的執行上下文(由於js是單線程語言),這個執行上下文是有一個棧來控制的,一般被稱爲執行棧或者調用棧。ide

執行棧是有LIFO(後進先出)特色的棧結構,事物只能從棧頂添加或者移出。 當前運行的執行上下文老是棧的最頂部,而且噹噹前運行的函數結束的時候,他的執行上下文會從棧頂彈出而後控制器到棧中的下一個執行上下文。函數

讓咱們看一個小的代碼片斷來更好的理解執行上下文和棧:工具

Execution Context Example

執行上下文例子

當代碼執行的時候,js引擎會建立一個全局的執行上下文來執行全局的代碼,當它碰到了對first()函數的調用,他爲函數建立了一個新的執行上下文並把它推入執行棧的棧頂。

因此上述代碼的執行棧以下圖所示

Execution Stack
Execution Stack

first()函數結束的時候,他的執行上下文從執行棧移出,控制器到達他下面的執行上下文也就是全局執行上下文。因此全局做用域中剩下的代碼將會被繼續執行。

詞法環境

每次JavaScript引擎建立一個執行上下文來執行函數或者全局代碼, 它同時也會建立一個新的詞法環境來存儲在函數執行過程當中定義在函數內部的變量。

詞法環境是一個保存標識符-變量的映射的數據結構。(此處標識符指的是變量或者函數的名字,變量是對實際對象[包括函數類型對象]或原始值的引用)

一個詞法環境要有兩部分組成:(1)環境記錄 以及 (2)一個對外部環境的引用

  1. 環境記錄是變量和函數聲明真實的存儲位置
  2. 對外部環境的引用覺得着它能夠訪問其外部詞法環境。這部分是理解閉包怎麼工做的最重要的部分。

一個詞法環境理論上應該長成這個樣子:

lexicalEnvironment = {
    environmentRecord: {
        <identifier> : <value>,
        <identifier> : <value>,
        <標識符> : <值>
    },
    outer: <Reference to the parent lexical environment>
    <!--outer:指向父詞法環境-->
}
複製代碼

因此讓咱們在看一遍上面的代碼塊:

let a = 'Hello world';
function first(){
    let b = 25;
    console.log('inside first function');
}

first();
console.log('inside global execution context');
複製代碼

當JavaScript引擎建立了一個全局的執行上下文來執行代碼的時候,它同時建立一個新的詞法環境來存儲那些定義在全局做用域中的變量和函數。 所以全局做用域的詞法環境應該長成這個樣子:

globalLexicalEnvironment = {
    environmentRecord:{
        a       : 'Hello world',
        first   : <reference to function object>
    },
    outer: null
}
複製代碼

在這裏外部的詞法環境被設置爲null由於沒有比全局做用域更外部的詞法環境。

當引擎建立first函數的執行上下文的同時,它也爲函數建立了一個詞法環境來存儲在執行函數的過程當中定義在函數內部的變量。所以函數的詞法環境應該是這個樣子:

functionLexicalEnvironment:{
    environmentRecord: {
        b : 25
    },
    outer: <globalLexicalEnvironment>
}
複製代碼

函數的外部詞法環境被設置爲全局詞法環境,由於函數在源碼中被全局做用域包含着。

注意- 當一個函數結束調用的時候,他的執行上下文被從棧頂移出,可是他的詞法環境可能也可能不從內存中移出 ,這取決於詞法環境實發被其餘詞法環境在他們的外部詞法環境引用。

一個更詳細的閉包例子:

如今咱們理解了執行上下文和詞法環境,讓咱們回到閉包。

Example1

讓咱們看一下下面的代碼片斷:

function Person(){
    let name = 'Peter';
    
    return function DisplayName(){
        console.log(name);
    };
}

let peter = person();
peter();//輸出 'peter'
複製代碼

person函數被執行的時候,JS引擎爲該函數建立了一個新的執行上下文和詞法環境。在函數結束以後,他返回displayName函數並把它分配給peter 變量。

所以它的詞法做用域長成這個樣子:

personLexicalEnvironment = {
  environmentRecord: {
    name : 'Peter',
    displayName: < displayName function reference>
  }
  outer: <globalLexicalEnvironment>
}
複製代碼

peter函數執行的時候(其實是對displayName 函數的引用),js引擎爲函數建立了一個新的執行上下文和詞法環境。

所以它的詞法環境長成這個樣子:

displayNameLexicalEnvironment = {
  environmentRecord: {
  }
  outer: <personLexicalEnvironment>
}
複製代碼

由於在displayName函數內部沒有私有變量,所以它的環境記錄是空的。在執行函數的過程當中,js引擎嘗試在他的詞法環境中尋找變量name。 由於在displayName函數的詞法做用域中沒有變量,因此引擎會在他的外部詞法環境尋找這個變量,也就是說,person函數的詞法環境仍是在內存中的。JS引擎找到了變量,並把name在控制檯輸出。

Example3

function getCounter(){
    let counter = 0;
    return function(){
        return counter++;
    }
}

let count = getCounter();
console.log(count());//0
console.log(count());//1
console.log(count());//2
複製代碼

再來一遍,getCounter函數的詞法環境應該長成這個樣子:

getCounterLexicalEnvironment = {
    environmentRecord: {
        counter: 0,
        <anonymous function>: <reference to function>
    },
    outer: <globalLexicalEnvironment>
}
複製代碼

這個函數返回了一個匿名函數並把它賦值給了count變量。

count函數被執行的時候,他的詞法做用域是這個樣子的:

countLexicalEnvironment = {
    environmentRecord: {
        
    },
    outer: <getCountLexicalEnvironment>
}
複製代碼

count函數調用的時候,JS引擎在該函數的詞法做用域裏面尋找了一下counter變量。他的環境記錄也是空的,引擎便會去他的外層詞法環境去找。

引擎找到了變量,把它輸出到控制檯,而後在getCounter函數的詞法做用域中增長了counter變量的值。

因此getCounter函數的詞法做用域在第一次調用count以後變成了這個樣子

getCounterLexicalEnvironment = {
    environmentRecord: {
        counter: 1,
        <anonymous function>: <reference to function>
    },
    outer: <globalLexicalEnvironment>
}
複製代碼

在每次的count函數調用以後,js建立了一個新的count的詞法做用域,遞增了counter變量而後更新了getCounter函數的詞法做用域來反應變化。

結論

因此咱們已經瞭解了什麼是閉包以及它們是如何工做的。 閉包是每一個JavaScript開發人員都應該理解的JavaScript的基本概念。 熟悉這些概念將有助於您成爲一個更有效,更好的JavaScript開發人員。

就是這樣,若是你發現這篇文章有用,請點擊下面的拍手👏按鈕,你也能夠在 社交媒體和Twitter上關注我,若是你有任何疑問,請隨時發表評論! 我很樂意幫忙:)

譯者注

新的一年,仍是要努力的提高本身:)祝你們新春快樂

相關文章
相關標籤/搜索