JavaScript深刻系列第八篇,介紹理論上的閉包和實踐上的閉包,以及從做用域鏈的角度解析經典的閉包題。git
MDN 對閉包的定義爲:github
閉包是指那些可以訪問自由變量的函數。面試
那什麼是自由變量呢?閉包
自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。app
由此,咱們能夠看出閉包共有兩部分組成:函數
閉包 = 函數 + 函數可以訪問的自由變量this
舉個例子:翻譯
var a = 1; function foo() { console.log(a); } foo();
foo 函數能夠訪問變量 a,可是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數,因此 a 就是自由變量。code
那麼,函數 foo + foo 函數訪問的自由變量 a 不就是構成了一個閉包嘛……對象
還真是這樣的!
因此在《JavaScript權威指南》中就講到:從技術的角度講,全部的JavaScript函數都是閉包。
咦,這怎麼跟咱們平時看到的講到的閉包不同呢!?
彆着急,這是理論上的閉包,其實還有一個實踐角度上的閉包,讓咱們看看湯姆大叔翻譯的關於閉包的文章中的定義:
ECMAScript中,閉包指的是:
從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。
從實踐角度:如下函數纔算是閉包:
即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
在代碼中引用了自由變量
接下來就來說講實踐上的閉包。
讓咱們先寫個例子,例子依然是來自《JavaScript權威指南》,稍微作點改動:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } var foo = checkscope(); foo();
首先咱們要分析一下這段代碼中執行上下文棧和執行上下文的變化狀況。
另外一個與這段代碼類似的例子,在《JavaScript深刻之執行上下文》中有着很是詳細的分析。若是看不懂如下的執行過程,建議先閱讀這篇文章。
這裏直接給出簡要的執行過程:
進入全局代碼,建立全局執行上下文,全局執行上下文壓入執行上下文棧
全局執行上下文初始化
執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 執行上下文被壓入執行上下文棧
checkscope 執行上下文初始化,建立變量對象、做用域鏈、this等
checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
執行 f 函數,建立 f 函數執行上下文,f 執行上下文被壓入執行上下文棧
f 執行上下文初始化,建立變量對象、做用域鏈、this等
f 函數執行完畢,f 函數上下文從執行上下文棧中彈出
瞭解到這個過程,咱們應該思考一個問題,那就是:
當 f 函數執行的時候,checkscope 函數上下文已經被銷燬了啊(即從執行上下文棧中被彈出),怎麼還會讀取到 checkscope 做用域下的 scope 值呢?
以上的代碼,要是轉換成 PHP,就會報錯,由於在 PHP 中,f 函數只能讀取到本身做用域和全局做用域裏的值,因此讀不到 checkscope 下的 scope 值。(這段我問的PHP同事……)
然而 JavaScript 倒是能夠的!
當咱們瞭解了具體的執行過程後,咱們知道 f 執行上下文維護了一個做用域鏈:
fContext = { Scope: [AO, checkscopeContext.AO, globalContext.VO], }
對的,就是由於這個做用域鏈,f 函數依然能夠讀取到 checkscopeContext.AO 的值,說明當 f 函數引用了 checkscopeContext.AO 中的值的時候,即便 checkscopeContext 被銷燬了,可是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中,f 函數依然能夠經過 f 函數的做用域鏈找到它,正是由於 JavaScript 作到了這一點,從而實現了閉包這個概念。
因此,讓咱們再看一遍實踐角度上閉包的定義:
即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
在代碼中引用了自由變量
在這裏再補充一個《JavaScript權威指南》英文原版對閉包的定義:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
閉包在計算機科學中也只是一個普通的概念,你們不要去想得太複雜。
接下來,看這道刷題必刷,面試必考的閉包題:
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
答案是都是 3,讓咱們分析一下緣由:
當執行到 data[0] 函數以前,此時全局上下文的 VO 爲:
globalContext = { VO: { data: [...], i: 3 } }
當執行 data[0] 函數的時候,data[0] 函數的做用域鏈爲:
data[0]Context = { Scope: [AO, globalContext.VO] }
data[0]Context 的 AO 並無 i 值,因此會從 globalContext.VO 中查找,i 爲 3,因此打印的結果就是 3。
data[1] 和 data[2] 是同樣的道理。
因此讓咱們改爲閉包看看:
var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2]();
當執行到 data[0] 函數以前,此時全局上下文的 VO 爲:
globalContext = { VO: { data: [...], i: 3 } }
跟沒改以前如出一轍。
當執行 data[0] 函數的時候,data[0] 函數的做用域鏈發生了改變:
data[0]Context = { Scope: [AO, 匿名函數Context.AO globalContext.VO] }
匿名函數執行上下文的 AO 爲:
匿名函數Context = { AO: { arguments: { 0: 0, length: 1 }, i: 0 } }
data[0]Context 的 AO 並無 i 值,因此會沿着做用域鏈從匿名函數 Context.AO 中查找,這時候就會找 i 爲 0,找到了就不會往 globalContext.VO 中查找了,即便 globalContext.VO 也有 i 的值(值爲3),因此打印的結果就是 0。
data[1] 和 data[2] 是同樣的道理。
若是想了解執行上下文的具體變化,不妨按部就班,閱讀這六篇:
《JavaScript深刻之從ECMAScript規範解讀this》
JavaScript深刻系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript深刻系列預計寫十五篇左右,旨在幫你們捋順JavaScript底層知識,重點講解如原型、做用域、執行上下文、變量對象、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎star,對做者也是一種鼓勵。