一 做用域相關
做用域是一套規則,用來管理引擎如何查找變量。在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的變化。