[譯]理解JS中的閉包
此篇文章翻譯自Sukhjinder Arora文章 Understanding Closures in JavaScript . 這篇文章結合了閉包,詞法作用域,呼叫棧以及執行上下文來理解閉包。文章如有翻譯不好的地方還望多多包涵。
理解JS中的閉包
閉包是每一個js開發者都需要知道和理解的概念。然而,它也是一個困擾著所有小萌新的概念。
如果對於閉包有正確理解的話,他會幫助你寫出更好更快更強的程式碼。那也就是說,它會幫助你成為一個更好js開發者。
因此在這個文章內,我將會嘗試解釋閉包的內部原理,以及他們是如何在實際js中執行的。
屁話不多說,我們開始吧:)
(廣告時間) 小貼士:當寫了可複用的js程式碼的時候,你可能想要不僅僅在一個專案中使用他們. Bit 是一個非常有用對對小工具方便你快速的分享和整理你的可複用程式碼,
執行上下文
執行上下文是js程式碼賦值和執行的抽象環境。當全域性程式碼執行的時候,他就執行在全域性執行上下文內部。函式程式碼執行在函式執行上下文內部。
js中有且只有一個當前正在執行的執行上下文(因為js是單執行緒語言),這個執行上下文是有一個棧來控制的,通常被稱為執行棧或者呼叫棧。
執行棧是有LIFO(後進先出)特點的棧結構,事物只能從棧頂新增或者移出。 當前執行的執行上下文總是棧的最頂部,並且噹噹前執行的函式結束的時候,他的執行上下文會從棧頂彈出然後控制器到棧中的下一個執行上下文。
讓我們看一個小的程式碼片段來更好的理解執行上下文和棧:

當代碼執行的時候,js引擎會建立一個全域性的執行上下文來執行全域性的程式碼,當它碰到了對 first()
函式的呼叫,他為函式建立了一個新的執行上下文並把它推入執行棧的棧頂。
所以上述程式碼的執行棧如下圖所示

當 first()
函式結束的時候,他的執行上下文從執行棧移出,控制器到達他下面的執行上下文也就是全域性執行上下文。所以全域性作用域中剩下的程式碼將會被繼續執行。
詞法環境
每次JavaScript引擎建立一個執行上下文來執行函式或者全域性程式碼, 它同時也會建立一個新的詞法環境來儲存在函式執行過程中定義在函式內部的變數。
詞法環境是一個儲存識別符號-變數的對映的資料結構。(此處 識別符號 指的是變數或者函式的名字, 變數 是對實際物件[包括函式型別物件]或原始值的引用)
一個詞法環境要有兩部分組成:(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開發人員。
就是這樣,如果你發現這篇文章有用,請點選下面的拍手:clap:按鈕,你也可以在 社交媒體和Twitter上關注我,如果你有任何疑問,請隨時發表評論! 我很樂意幫忙:)