javascript精雕細琢(三):作用域與作用域鏈
目錄
引言
作用域與作用域鏈是JS應用中無時無刻不在影響程式執行的關鍵屬性,但是由於它的不可見性,或者說它存在的過於普遍,簡直就像空氣一樣。所以對它的談及,都很簡單,而理解起來也不復雜。
但是由於它的重要性,對它做一個篇幅的說明,也是一件理所應當的事情。而且它其實也並沒有理解上那麼簡單。
本文對作用域及作用域鏈的說明,可能並不全面。筆者儘量以自身開發過程中的理解,做到表述全面。
1、執行環境
按照《JavaScript高階程式設計》第3版中的定義—— 執行環境 定義了變數或函式有權訪問其他資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的 變數物件 ,環境中定義的所有變數和函式都儲存在這個物件中。
對於環境的簡單理解,可以理解為 {}一個大括號就是一個執行環境 。如 {{}},就是包含關係的兩個執行環境 。而由於所有的函式和變數,最終都是通過最外圍有對應的呼叫,才能最終實現執行,所以將 最外圍的執行環境 定義為 全域性執行環境 ,在宿主為web瀏覽器時,全域性執行環境就為 window物件 。
每個函式都有自己的執行環境。當執行程式進入一個函式時,函式的環境就會被推入一個 環境棧 中。而在函式執行之後,棧將環境彈出,把控制權返回給之前的執行環境。之後,該環境因為 執行完畢 ,隨即被 銷燬 。但有一個例外, 全域性環境將永遠不會被銷燬,直到程式被關閉,如關閉瀏覽器。
由上圖我們清晰的看到JavaScript的兩個階段的執行流程。
2、作用域與作用域鏈
作用域鏈和作用域是一個包含關係。當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈。而作用域鏈中,包含的就是一個個有序排列的作用域。作用域鏈的作用,就是 保證執行環境中的變數和函式的有序訪問 。
每次的變數或函式宣告,都會形成一個 識別符號(即變數名或函式名)。 作用域鏈中的向上查詢,實際上就是識別符號的查詢。找到匹配的識別符號,就呼叫它的值。
作用域鏈中包含的每個作用域物件,其中都包含著它自身環境中可訪問的變數或函式(script作用域中,為全部的變數和函式;函式作用域中,子函式呼叫父函式環境中的變數或函式會形成閉包,那麼可訪問變數就為閉包呼叫的變數)。換一種角度理解的話,這些環境中可訪問的變數或函式就是識別符號,是當前作用域中可通過作用域鏈向上查詢的識別符號。
一大段的文字不光讀起來枯燥,理解起來也困難。所以,stop talking,show code。
以函式為例
function test() { let a = 1; //註釋此行,保留其他a,c()列印2,並且作用域鏈失去閉包物件 let b = 0; return () => { //形成閉包環境 console.log(a); } } let a = 2; //註釋此行及test中的a,c()列印3 window.a = 3;//註釋全部a,報錯 const c = test(); c(); console.dir(test); console.dir(c);
以上程式碼,通過 3次註釋 以及 chrome瀏覽器列印 ,可以清晰的看到作用域與作用域鏈的形成及作用域鏈*按順序查詢**的特性
1)首先是閉包呼叫——所有a都不註釋

由上圖可以看出,此時作用域鏈中存在 3個作用域物件。 按索引順序為 0—Closure閉包環境、1—Script標籤環境、2—Global全域性環境 ;
索引順序就是識別符號的查詢順序,所以當執行函式c()時,先在Closure閉包環境中查詢,然後是Script標籤作用域,最後是Global全域性作用域。 因為形成了閉包,那麼識別符號的查詢肯定能在閉包環境中找到對應結果,所以列印1 ;
值得注意的一點是,Closure與Script作用域物件中,儲存的識別符號的不同。 closure只儲存呼叫值a = 1,並沒有儲存b = 0 。而 Script中將環境中的所有識別符號都存在了物件中 ;
2)然後是取消閉包呼叫,改為Scriptbiaoqian作用域查詢——註釋test中的a = 1

由上圖可以看出,此時作用域鏈中的Closure閉包作用域消失,只剩下2個物件0—Script標籤環境、2—Global全域性環境;
那麼同樣按索引順序查詢,找到了Script標籤環境中的a = 2,沒有繼續尋找Global全域性環境中的window.a = 3,最終列印2;
3)最後是Global全域性作用域查詢——註釋test中的a = 1及script中的a = 2

與2)中情況對比,作用域鏈中的物件沒有改變。但是Script物件中的識別符號,隨著a = 2被註釋而消失
按索引順序查詢,找到了Script標籤環境中沒有a,繼續尋找Global全域性環境,最終找到window.a = 3,列印3;
函式test的列印,從始至終都是Script標籤作用域及Global全域性作用域。理解了c函式的作用域鏈,自然就明白了函式test的,所以不再展開贅述。