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

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

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

1.2 函數作用域

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

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

1.3 塊作用域

      es新增的關鍵字let,const是作用在塊級作用域。但是在js內{}形成的塊,是不具有作用域的概念的。如下所示,雖然for循環有一個{}包裹的塊,但是在塊外面還是可以訪問i的。
圖片描述

2 作用域鏈

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

      如上圖所示,會形成一個inner作用域到outer作用域到全局作用域的作用域鏈。當我們在執行inner函數的時候,需要outName的變量,在自己的作用域內找不到,便會順着作用域鏈往上找,直到找到全局作用域。在這個例子中,往上查找到outer作用域的時候便找到了。

      簡單測試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中引進了異步的機制。這塊具體的執行邏輯可以參考http://km.oa.com/articles/sho...。在這裏,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的變化。

圖片描述

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章