第一部分:做用域和閉包閉包
第一章:做用域是什麼異步
第二章:詞法做用域分佈式
第三章:函數做用域和塊做用域函數
第四章:提高性能
第五章:做用域閉包spa
一、簡單的歸納圖3d
二、3個簡單的demoblog
下面這幾個 demo 是考察你是否瞭解 JS引擎 在編譯和執行時的工做機制隊列
三、JS是編譯語言事件
第一章原文節選:
儘管一般將 JavaScript 歸類爲‘動態’或‘解釋執行語言’,但事實上,它是一門編譯語言。
但與傳統編譯語言不一樣,它不是提早編譯的,編譯結果也不能在分佈式系統中進行移植。
對於 JavaScript 來講,大部分狀況下編譯發生在代碼執行前的幾微秒(甚至更短!)
編譯時 JS引擎 會作哪些事:
簡單歸納:詞法分析 -> 語法分析(抽象語法樹 AST)-> 代碼生成(將 AST 轉換爲可執行代碼,供 JS引擎 執行階段使用)
編譯的詞法分析階段基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它們進行查找,其中具體的細節是這樣的:
3.1 若是遇到一個變量的聲明例如 var a,JS引擎會詢問做用域是否已經有一個該名稱的變量存在於同一個做用域的集合中?
若是是,JS引擎會忽略該聲明,繼續進行編譯,不然它會要求做用域在當前做用域的集合中聲明一個新的變量,並命名爲 a
3.2 若是變量(含函數表達式)是以 var 關鍵字聲明的,該變量的聲明會被提高到當前做用域的最頂部,變量的賦值語句則留在原先位置
3.3 若是是函數聲明,整個函數定義都會被提高到當前做用域的最頂部
3.4 若是當前做用域出現了同名的函數聲明和變量聲明,函數聲明優先被提高到做用域的最頂部,同名的變量聲明則會被忽略
3.5 示例分析
上面的代碼段會被引擎理解爲以下形式:
注意,var foo 儘管出如今 function foo() ...... 的聲明以前,但它是重複的聲明(所以被忽略了),由於函數聲明會被提高到普通變量以前
儘管重複的 var 聲明會被忽略掉,但出如今後面的對 foo 的賦值操做仍是能覆蓋前面的函數定義
擴展閱讀:
這篇文章來自知乎,裏面一些優秀的回答能幫助你加深對 JavaScript 是編譯語言的理解
《V8引擎本用了什麼編譯技術,使得 JavaScript 與用 C 寫出來的代碼性能相差無幾?》
四、JS是詞法做用域、JS引擎在編譯和執行階段都要和做用域打交道
做用域是一套規則,這套規則用來管理引擎如何在當前做用域以及嵌套的子做用域中根據標識符名稱進行變量查找
詞法做用域最重要的特徵是它的定義過程發生在代碼的書寫階段( 假設你沒有使用 eval() 或 with )
詞法做用域的造成階段主要發生在編譯階段,JS引擎在執行階段主要是進行做用域查詢:
以 var a = 2 爲例
JS引擎會在解釋 JavaScript 代碼以前首先對其進行編譯,JS是先編譯後執行
因此在 JS引擎 看來,var a = 2 不是一個聲明,而是兩個單獨的聲明 var a 和 a = 2
第一個聲明是編譯階段的任務,若是當前做用域下沒有 a 變量就建立,若是有,就忽略該聲明
第二個聲明是執行階段的任務,若是當前做用域下有 a 這個變量就將2賦值給a,若是沒有就去當前做用域的上一級做用域查找,以此類推,若是到了全局做用域仍是沒有找到a變量,引擎會拋出 ReferenceError
五、函數做用域、塊做用域
ES6以前,每建立一個函數就會產生一個新的基於函數的做用域,ES6出現後,JS開始擁有基於代碼塊( 一般指 { .. } 內部)的做用域
塊做用域:
特色:編譯時,塊做用域裏的變量的聲明不會被提高
優勢:
5.一、垃圾收集 ( 原書3.4節 )
5.二、let 循環
5.2.1 var 循環
咱們指望的結果多是這樣的:
先執行 for 循環,由於循環2次,因此第一次循環打印 i 其值爲0,第二次循環打印 i 其值爲1,循環結束後打印 i 其值爲 2
分析爲何指望和運行的結果不一致:
當既有同步代碼也有異步代碼時,引擎會先執行同步代碼,全部的同步代碼都執行完畢後,再回過頭去執行異步代碼
第一次 for 循環時,循環體是一個定時器它是異步的,因此引擎暫時不會執行它,會將它放到事件隊列中。
此時循環計數 i 由 0 變爲 1,知足循環條件,能夠進行第二次循環
由於此處的 for循環 的循環體不管循環多少次,循環體都不變仍是定時器,因此第二次循環時,引擎又將一個定時器放到事件隊列中
此時循環計數 i 由 1 變爲 2,不知足循環條件,循環結束
for循環後是一條console語句,它是同步的,因此引擎會直接執行它,引擎會在當前做用域即全局做用域查找變量 i,找到後打印其值爲2
這段程序的全部同步代碼都執行完畢了,引擎開始去處理哪些存在事件隊列中而且知足當下執行條件的異步代碼,找到符合要求的兩個定時器
這兩個定時器都要打印變量 i 的值,從詞法做用域能夠看出,它們要打印的就是全局變量 i 的值,而全局變量 i 的值如今是2
因此運行的結果是,先執行了 for循環 以後的console語句,打印2,最後執行兩個定時器且打印的結果都是2
5.2.2 let 循環
爲何 let 循環運行後的結果就能符合咱們的預期呢?
分析:
和 var 循環同樣,每次循環體裏的定時器都會被放到事件隊列中,等程序全部的同步代碼都執行完畢後,纔會去執行事件隊列中的異步代碼
循環結束後沒有同步代碼可執行,並且定時器的延遲時間咱們設置的是 0ms,因此循環一結束,引擎就會立馬執行那兩個定時器的回調函數
這兩個定時器的回調函數都要打印一個變量 i 的值,說明這個變量 i 的值必定不是全局變量,若是是全局變量兩次打印的值確定是同樣的
但 let循環 實際運行兩次打印的值都不同,因此這個變量 i 究竟是什麼做用域呢,既不是全局做用域,也不是函數做用域(for循環代碼外面沒有函數包裹)
是塊做用域
for 循環頭部的 let 聲明還會有一個特殊的行爲:
每次迭代循環計數 i 都會被從新聲明一次,因此 let 循環每次迭代都會生成一個全新的封閉的塊做用域,實際效果相似下方代碼
以第一個定時器回調函數爲例,它須要在做用域裏找到變量 j 才能完成 console 語句。
JS是詞法做用域,做用域在代碼書寫的時候就定義好了
以 let 關鍵字聲明的變量會生成塊做用域,在 { ... } 塊內的確有一個變量 j,其值被賦值爲每次迭代開始的循環計數
每循環一次就會執行一次 let j,就會生成一個新的塊做用域,因此兩個回調函數都打印了 j 變量,但這兩個 j 變量不是同一個變量
循環結束後這些聲明的 j 變量對應的內存空間其實就會被銷燬,但由於在同一個塊做用域裏,定時器的回調函數使用了這個應該隨後會被銷燬的 j 變量,
因此每次循環產生的塊做用域裏的變量都還保留着,因此當引擎開始執行定時器的回調時,每一個回調都能找到本身的 j 變量
此處的 let循環 除了涉及塊做用域仍是涉及到閉包,下方會有閉包的介紹
5.3 改造 var 循環
明白了 let循環 爲何能達到預期結果後,咱們不妨試着改造以前的 var 循環,不是沒有塊做用域就完成不了需求
分析:
5.3.1 由於指望每循環一次打印的都是當前的循環計數,即每次循環打印的值都不同
5.3.2 因此變量 i 必定不是全局變量,不是全局變量只能是局部變量,即變量 i 屬於函數做用域
5.3.3 因此循環體外層要包裹一個函數
5.3.4 也許是考慮到函數聲明會污染全局變量且手動調用很麻煩,因此下面這種形式更常見
六、閉包
閉包的2個示例
示例1:
示例2:
比較這兩個示例的共同點:
示例1中,foo() 函數執行完畢後,按理說 foo() 整個內部做用域都應該被銷燬
示例2中,wait() 函數執行完畢後,其產生的函數做用域也應該被銷燬
但2個示例中,本來在執行完畢後應該被釋放的內存空間都沒有被釋放
由於有特殊狀況出現,阻止了JS引擎的垃圾回收工做
什麼特殊狀況?
以示例1爲例:
foo() 函數內部有一個函數 bar(),bar() 函數顯然能訪問它的父做用域 foo() 函數
而 foo() 函數的返回值剛好就是其內部函數 bar()
咱們在全局做用域中調用 foo() 函數 並將其返回值賦值給了一個全局變量 baz
本來foo的內部函數bar只有一個引用計數,通過 var baz = foo() 後,如今bar函數的引用計數爲2
因此在 foo函數 執行完畢後,引擎沒有回收 bar函數 的內存空間,而 bar函數 裏又訪問了其父級做用域 foo函數 裏的變量,因此 foo函數 的內存空間暫時也不能釋放
全局變量baz是個函數表達式,baz() 後,咱們能打印 foo函數 的局部變量 a
閉包的定義
原書定義:
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包
閉包是基於詞法做用域書寫代碼時所產生的天然結果
我本身的話:
這種在某個具備封閉性質的詞法做用域以外由於一些緣由在外部還能訪問該做用域的能力就叫閉包
這些緣由有:
一個函數的返回值是其內部的一個子函數
在定時器、事件監聽器、Ajax請求、跨窗口通訊、Web Workers或者任何其餘的異步(或者同步)任務中使用了回調函數
IIFE模式是閉包嗎?
按照上述對閉包的定義,它不是閉包。
由於函數( 示例代碼中的IIFE )並非在它自己的詞法做用域之外執行的。
它在定義時所在的做用域而非外部做用域中執行
a 也是經過普通的詞法做用域查找而非閉包被發現的