結合做用域,執行上下文圖解閉包

一 做用域相關
      做用域是一套規則,用來管理引擎如何查找變量。在es5以前,js只有全局做用域及函數做用域。es6引入了塊級做用域。可是這個塊級別做用域須要注意的是否是{}的做用域,而是let,const關鍵字的塊做用域。es6

1做用域
1.1 全局做用域
      在全局環境下定義的變量,是掛載在window下的。以下代碼所示:
圖片描述面試

1.2 函數做用域chrome

      在函數內定義的變量,值在函數內部才生效,在函數外引用會報RefrenceError的錯誤
圖片描述segmentfault

      注意區分RefrenceError及TypeError。RefrenceError是在做用域內找不到,而TypeError則是類型錯誤。若是隻是定義了變量a 直接調用便會報TypeError的錯誤。
圖片描述
圖片描述瀏覽器

1.3 塊做用域閉包

      es新增的關鍵字let,const是做用在塊級做用域。可是在js內{}造成的塊,是不具備做用域的概念的。以下所示,雖然for循環有一個{}包裹的塊,可是在塊外面仍是能夠訪問i的。
圖片描述異步

2 做用域鏈函數

      所謂做用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證當前執行環境對符合訪問權限的變量和函數的有序訪問。而做用域的最大的用處就是隔離變量,不一樣做用域下同名變量不會有衝突。
圖片描述測試

      如上圖所示,會造成一個inner做用域到outer做用域到全局做用域的做用域鏈。當咱們在執行inner函數的時候,須要outName的變量,在本身的做用域內找不到,便會順着做用域鏈往上找,直到找到全局做用域。在這個例子中,往上查找到outer做用域的時候便找到了。this

      簡單測試1:以下圖所示的代碼,你們以爲會輸出什麼呢?
圖片描述
      雖然fn的調用是在show內調用的,可是由於fn所在的做用域是全局做用域,它的x的值會順着做用域鏈去全局做用域中啊,即x會輸出10。這裏須要注意的一點是,變量的肯定是在函數定義時候肯定的,而不是函數運行時。

二 執行上下文相關

      函數每次被調用時,都會產生一個新的執行上下文環境。全局上下文是存在棧中的。而處於棧頂的全局上下文一旦執行完就會自動出棧。以下圖所示的代碼。
圖片描述

      首先是全局上下文入棧,而後開始執行可執行代碼。遇到outer(),激活outer()的上下文;

      第二步,outer的上下文入棧。開始執行outer內的可執行代碼,直到遇到inner()。激活inner()的上下文;

      第三步,inner的上下文入棧。開始執行inner內的可執行代碼。執行完畢以後inner出棧。

      第四步,inner的上下文出棧。outer內繼續執行可執行代碼。若是一直沒有其餘的執行上下文,執行完畢便可出棧;

      第五步,outer的上下文出棧。

      ps:全局上下文只有瀏覽器關閉的時候纔會出棧。

圖片描述

      那咱們已經直到了全局上下文的宏觀入棧出棧的概念。具體的全局上下文包括哪些內容,具體作了什麼操做呢?

      其實,執行上下文分爲準備階段和執行階段。

      1.在執行上下文的準備階段,會有如下步驟:

            1.1 建立變量對象:初始化arguments,函數聲明提高,變量聲明提高等

            1.3 創建做用域鏈

     2.而在執行上下文的執行階段,會有如下步驟:

            2.1 變量賦值

            2.2 函數引用

            2.3 肯定this指向

            2.4 執行代碼

      而在變量對象的建立過程,會經歷如下的步驟。

            1.建立arguments對象。也就是當前上下文中的參數;

            2.檢查當前上下文的函數聲明,即用function關鍵字聲明的函數;

            3.檢查當前上下文的變量聲明,即變量,屬性值爲undefined。

      而這個建立過程最重要的概念就是提高:
圖片描述

      而以下圖所示的代碼執行,變量對象的變化過程是怎樣的呢?
圖片描述
      那函數內的三個console分別會輸出什麼呢?
      由於在變量對象的建立過程當中,是arguments=>函數聲明=>變量聲明的過程。在第一個console以前function foo()已經被提高,所以第一次輸出的該函數,而第二個console以前bar被提高,並賦值爲undefined,所以第二次輸出的是undefined。而第三個console以前foo被從新賦值,所以第三個console是'hello'。

      總結起來,變量對象和活動對象實際上是同一個對象,他們只是在執行上下文的不一樣階段的狀態而已。
圖片描述
      下面的截圖便是兩個階段的變化。其實變量對象和活動對象是同一個對象,他們只是執行上下文在不一樣階段的不一樣表現形式。在執行階段變量對象V0會變成活動對象A0。內部的一些引用也會發生變化。
圖片描述
      而以下圖所示的代碼執行,分別會輸出什麼呢?
圖片描述

      首先,第一段代碼。函數聲明首先會被提高第一個console輸出hello world。可是後面的hello會被覆蓋,第二個console輸出hello

      第二段代碼。函數聲明首先會被提高,可是緊接着會被變量賦值覆蓋。所以,兩個console輸出hello。
總結起來,全局上下文的整個過程即下圖所示

圖片描述

      那結合做用域即全局上下文呢,咱們一開始的代碼代碼具體的圖解就是下面這張圖了。

圖片描述

三 閉包相關
1 閉包分析

      此時,當咱們修改inner函數,返回上級做用域的outerName屬性時,閉包就產生了。

圖片描述

      這裏爲何會產生閉包呢?具體能夠參考下方的圖示。
      前面的全局入棧和outer函數入棧仍是跟原來同樣,可是當咱們的outer函數入棧執行完畢準備出棧,準備被回收的時候,因爲outName還被inner的做用域引用,不能被回收,產生了閉包。
圖片描述
      即所謂的閉包就是經過函數調用,外部持有函數的句柄,讓函數的空間不能消失。產生的這塊獨體的空間永遠存在,這塊內存對外也是封閉的。因此就叫閉包。

2 常見問題分析

      相信你們在面試的時候會常常問到這樣的面試題。下面這段代碼輸入的是什麼呢?
圖片描述

      這裏輸出的是5個6。須要解釋這個問題呢,要涉及到js的的執行環境及做用域鏈了。

      js的執行環境:JS是單線程環境,即代碼的執行是從上到下,依次執行。這樣的執行稱爲同步執行。由於種種不要浪費和節約的緣由。JS中引進了異步的機制。這塊具體的執行邏輯能夠參考https://segmentfault.com/a/11...。在這裏,for循環是同步代碼,會先從上到下執行。而setTimeout中的是異步代碼會將其插入到任務隊列當中等待。所以在setTimeout執行的時候,for循環已經執行完成,i已經變成6。做用域鏈。當setTimeout執行的時候,會向上去查找i的值。往上查找,即for所在的做用域,已是6了。所以6次setTimeout都會輸出6。

      那可能面試官會繼續問,咱們怎樣才能依次輸出1-5呢?這裏就能夠用到閉包來解決了。
圖片描述

      咱們將i做爲參數傳遞,而且造成了一個新的當即執行函數做用域。當setTimeout執行的時候,去查找i。即在當即執行函數做用域查找,此時的i咱們能夠根據上面一部分的分析,造成了閉包以後,它的內存是不會消失的。所以這每次循環的時候都是當前i即1-5。

3 閉包的查看

      其實,咱們在chrome的控制檯是能夠去查看閉包的。在瀏覽器斷點調試,能夠去觀察下面兩幅圖的紅色圈區別。第二副圖能夠看到closure,i值是1。依次執行,能夠看到i從1到5的變化。

圖片描述

相關文章
相關標籤/搜索