為什麼js會有閉包

沈江平發表於2018-07-23

前兩天阿里的面試,gg了。不過還是收穫良多。阿里的面試沒有怎麼聊到框架,都是原生和底層的東西比較多。

期間,對面大哥問我,為什麼js會有閉包?

我一愣,說我理解的是,js的閉包其實是對js函式作用域特性的一種利用,因為函式內定義的區域性變數不能被外部直接獲取,而函式卻可以訪問到其外部作用域的變數。所以我們可以在函式內部定義一個訪問區域性變數的方法並將之輸出給外部。在函式銷燬後,通過函式輸出的方法訪問區域性變數成了唯一途徑。(當時大概說了這些,其實答的是“什麼是閉包”。。。)

對面沒說話。

我試探性地補充說:通過閉包,我們可以避免全域性汙染,保護私密資料云云。

對面沒說話。

我急了:呃。。我們平時建立的自執行匿名函式也是一種閉包,解析器讀到那對括號,還將至視為一個表示式立即執行blabla

對面說:這些都不是我想聽的答案,能從更底層一些的角度講講嗎?

我:哈。。。

後來也問到js引擎和node的記憶體管理。

 

週末的時候重新看了下js引擎的工作過程,對這個問題有了新的認識。簡單來說,js引擎的工作分兩個階段,一個是語法檢查階段,一個是執行階段。而執行階段又分預解析和執行兩個階段。

在預解析階段,先建立執行上下文,執行上下文包括變數物件、作用域鏈和this值。

    變數物件VO:var宣告的變數、function宣告的函式,及當前函式的形參

    作用域鏈:當前變數物件+所有父級作用域 [[scope]]

    this值:在進入執行上下文後不再改變

    PS:作用域鏈其實就是一個變數物件的鏈,函式的變數物件稱之為active object,簡稱AO。函式建立後就有靜態的[[scope]]屬性,直到函式銷燬)

    建立執行上下文後,會對變數物件的屬性進行填充。所謂屬性,就是var、function宣告的標誌符及函式形參名,至於屬性對應的值:變數值為undefined,函式值為函式定義,形參值為實參,沒有傳入實參則為undefined。

預解析階段結束後,進入執行程式碼階段,此時執行上下文有個Scope屬性(區別於函式的[[scope]]屬性)。

  Scope = 當前AO.concat([[scope]])

js解析器逐行讀取並執行程式碼,變數物件中的屬性值可能因賦值語句而改變。當我們查詢外部作用域的變數時,其實就是沿著作用域鏈,依次在這些變數物件裡遍歷標誌符,直到最後的全域性變數物件。

 

回到閉包這個問題。當別人問你為什麼會有閉包這東西的時候,其實是在問,閉包的形成機制。

  function outer(){
	var a1=0;
	return function inner(){
		return a1;
	  }
  }

  function fn(){
    var getInnerData = outer();
    console.dir(getInnerData)
  }
   fn();

      當我們呼叫一個閉包函式時(比如上面的getInnerDate函式),因為函式執行時,其上下文有個Scope屬性,該屬性作為一個作用域鏈包含有該函式被定義時所有外層的變數物件的引用,所以定義了閉包的函式雖然銷燬了,但是其變數物件依然被繫結在函式inner上,保留在記憶體中。

事實上,只要程式碼保持對getInnerDate函式的引用,函式自身的[[scope]]屬性就繫結著閉包的活動物件。

但要留意的是,基於js的垃圾回收機制,outer的變數物件裡,只有仍被引用的變數會繼續儲存在記憶體中:

 

參考:

淺談javascript解析引擎解析過程

JS中的作用域鏈是在什麼時候建立的?

 

相關文章