什麼是閉包html
一個函數和對其周圍狀態(詞法環境)的引用捆綁在一塊兒,這樣的組合就是閉包。
也就是說,閉包讓你能夠在一個內層函數中訪問到其外層函數的做用域。
在 JavaScript 中,每當建立一個函數,閉包就會在函數建立的同時被建立出來。git
上面是MDN對閉包
的解釋,這幾句話可能不太好懂,不要緊,咱們先來看下能懂的:github
閉包
是和函數有關閉包
雖然從定義來看,全部函數均可以稱爲閉包
,可是當咱們在討論它的時候,通常是指這種狀況:瀏覽器
//code-01 function cat() { var name = "小貓"; function say() { console.log(`my name is ${name}`); } return say; } var fun = cat(); //---cat函數已經執行完,下面卻還可以訪問到 say函數的內部變量 name fun(); //> my name is 小貓
當一個函數的返回值是一個內部函數時(cat函數返回say函數),在這個函數已經執行完畢後,這個返回的內部函數還能夠訪問到已經執行完畢的函數的內部變量,就像 code-01
中fun能夠訪問到cat函數的name,通常咱們談論的閉包
就是指這種狀況。
那麼這是什麼緣由呢?這就涉及到函數的做用域鏈
和執行上下文
的概念了,咱們下面分別來講。閉包
定義
什麼是執行上下(Execution context )呢?簡單來講就是全局代碼或函數代碼執行的時候的環境,它包含三個部份內容:函數
咱們用一個對象來表示:oop
EC = { vo:{}, sc:[], this }
而後代碼或函數須要什麼變量的時候,就會在這裏面找。this
建立時間
執行上下文(EC)是何時建立的呢?這裏分爲兩種狀況:code
其實若是把全局的代碼理解爲一個大的函數,這二者就能夠統一了。
每個函數都會建立本身的執行上下文
,他們以棧的形式存儲在一塊兒,當函數執行完畢,則把它本身的執行上下文
出棧,這就叫執行上下文棧
(Execution context stack,ECS)
下面咱們經過一段代碼實例來看一下htm
聲明語句與變量提高
具體分析以前,咱們先來講聲明語句
,什麼是聲明語句
呢?
聲明語句
是用來聲明一個變量,函數,類的語句變量提高
,其餘不會,若是var和function同名的話,則函數聲明優先// code-02 console.log(varVal); // 輸出undefined console.log(fun); // 輸出 fun(){console.log('我是函數體') }, //console.log(letVal) //報錯 letVal is not defined var varVal = "var 聲明的變量"; let letVal = "let 聲明的變量"; function fun() { console.log("我是函數體"); } var fun = "function"; //與函數同名,函數優先,可是能夠從新賦值 console.log(varVal); // >> "var 聲明的變量" console.log(letVal); // >> "let 聲明的變量" //fun(); // 報錯,由於fun被賦值爲'function'字符串了 var name = "xiaoming";
在js執行代碼的時候,會先掃一遍代碼,把var,function的聲明先執行,var聲明的變量會先賦值爲undefined,function聲明的函數會直接就是函數體,這就叫變量提高
,而其餘的聲明,好比let,則不會。
因此在變量賦值以前,console.log(varVal)
,console.log(fun)
能夠執行,而console.log(letVal)
則會報錯。
其中fun被從新聲明爲'function'字符串,可是在變量提高的時候,函數優先,因此console.log(fun)
打印出來的是函數體,而代碼執行賦值語句的時候,fun被賦值成了字符串,因此fun()
會報錯
代碼執行過程分析--變量對象
咱們先上一段簡單的代碼,經過這段代碼,來分析一下 執行上下文
建立和做用的過程,對其內容咱們先只涉及變量對象
vo:
//code-03 var name = 'xiaoming' function user(name){ var age = 27 console.log(`我叫${name},今年${age}`) } user(name) console.log(name)
咱們如今來分析一下這段代碼執行過程當中,執行上下文的做用過程,會加入變量對象
vo,做用域鏈
scope會在下面講,this的指向此次不講,因此就不加上去了
1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS
ECS = [ G_EC : { vo:{ name:undefined, user(name){ var age = 27 console.log(`我叫${name},今年${age}`) }, }, sc } ]
2.代碼開始執行,name被賦值,執行user(name)
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文
user_EC,並壓入ECS
ECS = [ user_EC : { vo:{ name:undefined, age:undefined, }, sc }, G_EC : { vo:{ name:'xiaoming', user(name){ var age = 27 console.log(`我叫${name},今年${age}`) } }, sc } ]
4.開始執行函數代碼,給形參name賦值,變量age賦值,執行console.log的時候須要變量name
,age
,因而從它本身的執行上下文
user_EC中的變量對象
vo裏開始查找
ECS = [ user_EC : { vo:{ name:'xiaoming', age:27, }, sc }, G_EC : { vo:{ name:'xiaoming', user(name){ var age = 27 console.log(`我叫${name},今年${age}`) } }, sc } ]
5.發現找到了,因而打印 我叫xiaoming,今年27
,至此函數user執行完畢了,因而把其對應的執行上下文
user_EC出棧
ECS = [ G_EC : { vo:{ name:'xiaoming', user(name){ var age = 27 console.log(`我叫${name},今年${age}`) } }, sc } ]
6.代碼繼續執行,console.log(name),發現須要變量那麼,因而從它本身的執行上下文
中的變量對象
開始查找,也就是G_EC中的vo,順利找到,因而打印"xiaoming"
7.至此代碼執行結束,但全局的執行上下文好像要等到當前頁面關閉纔出棧(瀏覽器環境)
上面咱們分析代碼執行過程的時候,有說到若是要用到變量的時候,就從當前執行上下文
中的變量對象
vo裏查找,咱們恰好是都有找到。
那麼若是當前執行上下文
中的變量對象
中沒有須要用的變量呢?根據咱們的經驗,它會從父級的做用域來查找,那麼這是根據什麼來查找的呢?
全部接下來咱們繼續來看 '做用域鏈'(scope chain,sc),它也是執行上下文
得另外一個組成部分。
** 函數做用域 **
在說執行上下
中的做用域鏈
以前,咱們要先來看看函數做用域
,那麼這是個什麼東西呢?
執行上下
中的變量對象
存在其中//code-04 function fun_1(){ function fun_2(){} }
1.咱們看上面的代碼,當fun_1函數建立的時候,它的父級執行上下文
是全局執行上下文 G_EC
,因此fun_1的函數做用域
【scope】爲:
fun_1.scope = [ G_EC.vo ]
2.當fun_2函數建立的時候,它的全部父級執行上下文
有兩個,一個是全局執行上下文 G_EC
, 還有一個是函數fun_1的執行上下文 fun_1_EC
, 因此fun_2的函數做用域
【scope】爲:
fun_1.scope = [ fun_1_EC.vo, G_EC.vo ]
執行上下文的做用域鏈
上面咱們說的是函數做用域
,它包含了全部父級執行上下的變量對象,可是咱們發現它沒有包含函數本身的變量對象,由於這個時候函數只是聲明瞭,尚未執行,而函數的執行上下文
是在函數執行的時候建立的。
當函數執行的時候,會建立函數的執行上下文
,從上面咱們知道,這個時候會建立執行上下文
的變量對象
vo,而賦值執行上下文
的做用域鏈
sc的時候,會把vo加在scope前面,做爲一個隊列,賦值給做用域鏈
,
就是說:EC.sc = [EC.vo,...fun.scope]
,咱們下面舉例說明,這段代碼與code-03的區別只是不給函數傳參,因此會用到父級做用域的變量。
//code-05 var name = 'xiaoming' function user(){ var age = 27 console.log(`我叫${name},今年${age}`) } user() console.log(name)
1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS,同時賦值變量對象
vo、做用域鏈
sc,注意:當函數user被聲明的時候,會帶有函數做用域
user.scope
ECS = [ G_EC : { vo:{ name:undefined, user // user.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
2.代碼開始執行,name被賦值,執行user()
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文
user_EC,並壓入ECS,同時賦值變量對象
vo和做用域鏈
sc:
ECS = [ user_EC : { vo:{ age:undefined, }, sc:[user_EC.vo, ...user.scope] }, G_EC : { vo:{ name:'xiaoming', user // user.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
4.開始執行函數代碼,給變量age賦值,執行console.log的時候須要變量name
,age
,這裏咱們上面說是從變量對象
裏找,這裏更正一下,實際上是從做用域鏈
中查找
ECS = [ user_EC : { vo:{ age:27, }, sc:[user_EC.vo, ...user.scope] }, G_EC : { vo:{ name:'xiaoming', user, // user.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
5.咱們發如今做用域鏈
的第一個對象中(user_EC.vo)找到了age,可是沒有name
,因而開始查找做用域鏈
的第二個對象,依次往下找,若是都沒找到,則會報錯。
這裏的話,咱們發現做用域鏈
的第二個元素user.scope析構出來的,也就是G_EC.vo,這個裏面有找到name='xiaoming'
因而打印 我叫xiaoming,今年27
,至此函數user執行完畢了,因而把其對應的執行上下文
user_EC出棧
ECS = [ G_EC : { vo:{ name:'xiaoming', user, // user.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
6.代碼繼續執行,console.log(name),發現須要變量那麼,因而從它本身的執行上下文
中的做用域鏈
開始查找,在第一個元素G_EC.vo就順利找到,因而打印"xiaoming"
7.至此代碼執行結束,
到此爲止咱們介紹完了執行上下
文,那麼如今咱們迴歸到剛開始的閉包
爲何能訪問到已經執行完畢了的函數的內部變量問題。咱們再來回顧一下代碼:
//code-06 function cat() { var name = "小貓"; function say() { console.log(`my name is ${name}`); } return say; } var fun = cat(); fun();
咱們來照上面的步驟來分析下代碼:
1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS,同時賦值變量對象
vo、做用域鏈
sc
ECS = [ G_EC : { vo:{ fun:undefined, cat, // cat.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
2.代碼開始執行,執行cat()函數
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文
cat_EC,並壓入ECS,同時賦值變量對象
vo和做用域鏈
sc:
ECS = [ cat_EC : { vo:{ name:undefined, say, // say.scope:[cat_EC.vo,G_EC.vo] }, sc:[cat_EC.vo, ...cat.scope] }, G_EC : { vo:{ fun:undefined, cat, // cat.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
4.開始執行函數代碼,給變量name賦值,而後返回say函數,這個時候函數執行完畢,它的值被付給變量fun,它的執行上下文
出棧
ECS = [ G_EC : { vo:{ fun:say, // say.scope:[cat_EC.vo,G_EC.vo] cat // cat.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
5.代碼繼續執行,到了fun(),
6.當函數要執行,還沒執行具體代碼以前,建立函數執行上下文
fun_EC,並壓入ECS,同時賦值變量對象
vo和做用域鏈
sc:
ECS = [ fun_EC : { vo:{}, sc:[fun_EC.vo, ...fun.scope]//fun==cat,因此fun.scope = say.scope = [cat_EC.vo,G_EC.vo] }, G_EC : { vo:{ fun:say, // say.scope:[cat_EC.vo,G_EC.vo] cat // cat.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
7.函數fun開始執行具體代碼:console.log(
my name is ${name})
,發現須要變量name
,因而從他的fun_EC.sc中開始查找,第一個fun_EC.vo沒有,因而找第二個cat_EC.vo,發現這裏有name="小貓",
因而打印 my name is 小貓
,至此函數fun執行完畢了,因而把其對應的執行上下文
fun_EC出棧
ECS = [ G_EC : { vo:{ fun:say, // say.scope:[cat_EC.vo,G_EC.vo] cat // cat.scope:[G_EC.vo] }, sc:[G_EC.vo] } ]
8.至此代碼執行結束
到這裏咱們知道閉包
爲何能夠訪問到已經執行完畢的函數的內部變量,是由於在的執行上下文
中的做用域鏈
中保存了變量的引用,而保存的引用的變量不會被垃圾回收機制所銷燬。
閉包的優缺點
優勢:
缺點:
參考
1.JavaScript深刻之詞法做用域和動態做用域
2.JavaScript深刻之執行上下文棧
3.setTimeout和setImmediate到底誰先執行,本文讓你完全理解Event Loop