理解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開發人員。
就是這樣,如果你發現這篇文章有用,請點選下面的拍手?按鈕,你也可以在 社交媒體和Twitter上關注我,如果你有任何疑問,請隨時發表評論! 我很樂意幫忙:)
譯者注
新的一年,還是要努力的提升自己:)祝大家新春快樂