1. 程式人生 > >深入理解javascript作用域系列第五篇——一張圖理解執行環境和作用域

深入理解javascript作用域系列第五篇——一張圖理解執行環境和作用域

前面的話

  對於執行環境(execution context)和作用域(scope)並不容易區分,甚至很多人認為它們就是一回事,只是高程和犀牛書關於作用域的兩種不同翻譯而已。但實際上,它們並不相同,卻相互糾纏在一起。本文先用一張圖開宗明義,然後進行術語的簡單解釋,最後根據圖示內容進行詳細說明

圖示

檢視大圖

概念

【作用域】

  作用域是一套規則,用於確定在何處以及如何查詢識別符號。關於LHS查詢和RHS查詢詳見作用域系列第一篇內部原理

  作用域分為詞法作用域動態作用域。javascript使用詞法作用域,簡單地說,詞法作用域就是定義在詞法階段的作用域,是由寫程式碼時將變數和函式寫在哪裡來決定的。於是詞法作用域也可以描述為程式原始碼中定義變數和函式的區域

  作用域分為全域性作用域和函式作用域,函式作用域可以互相巢狀

  在下面的例子中,存在著全域性作用域,fn作用域和bar作用域,它們相互巢狀

【作用域鏈和自由變數】

  各個作用域的巢狀關係組成了一條作用域鏈。例子中bar函式儲存的作用域鏈是bar -> fn -> 全域性,fn函式儲存的作用域鏈是fn -> 全域性

  使用作用域鏈主要是進行識別符號的查詢,識別符號解析就是沿著作用域鏈一級一級地搜尋識別符號的過程,而作用域鏈就是要保證對變數和函式的有序訪問

  【1】如果自身作用域中聲明瞭該變數,則無需使用作用域鏈

  在下面的例子中,如果要在bar函式中查詢變數a,則直接使用

LHS查詢,賦值為100即可

var a = 1;
var b = 2;
function fn(x){
    var a = 10;
    function bar(x){
        var a = 100;
        b = x + a;
        return b;
    }
    bar(20);
    bar(200);
}
fn(0);

  【2】如果自身作用域中未宣告該變數,則需要使用作用域鏈進行查詢

  這時,就引出了另一個概念——自由變數。在當前作用域中存在但未在當前作用域中宣告的變數叫自由變數

  在下面的例子中,如果要在bar函式中查詢變數b,由於b並沒有在當前作用域中宣告,所以b是自由變數。bar函式的作用域鏈是bar -> fn -> 全域性。到上一級fn作用域中查詢b沒有找到,繼續到再上一級全域性作用域中查詢b,找到了b

var a = 1;
var b = 2;
function fn(x){
    var a = 10;
    function bar(x){
        var a = 100;
        b = x + a;
        return b;
    }
    bar(20);
    bar(200);
}
fn(0);

  [注意]如果識別符號沒有找到,則需要分為RHS和LHS查詢進行分析,若進行的是LHS查詢,則在全域性環境中宣告該變數,若是嚴格模式下的LHS查詢,則丟擲ReferenceError(引用錯誤)異常;若進行的是RHS查詢,則丟擲ReferenceError(引用錯誤)異常。詳細情況移步至此

【執行環境】

  執行環境(execution context),有時也稱為執行上下文、執行上下文環境或環境,定義了變數或函式有權訪問的其他資料。每個執行環境都有一個與之關聯的變數物件(variable object),環境中定義的所有變數和函式都儲存在這個物件中

  一定要區分執行環境和變數物件。執行環境會隨著函式的呼叫和返回,不斷的重建和銷燬。但變數物件在有變數引用(如閉包)的情況下,將留在記憶體中不被銷燬

  這是例子中的程式碼執行到第15行時fn(0)函式的執行環境,執行環境裡的變數物件儲存了fn()函式作用域內所有的變數和函式的值

【執行流】

  程式碼的執行順序叫做執行流,程式原始碼並不是按照程式碼的書寫順序一行一行往下執行,而是和函式的呼叫順序有關

  例子中的執行流是第1行 -> 第2行 -> 第4行 -> 第15行 -> 第5行 -> 第7行 -> 第12行 -> 第8行 -> 第9行 -> 第10行 -> 第11行 -> 第13行 -> 第8行 -> 第9行 -> 第10行 -> 第11行 -> 第14行

  [注意]在程式程式碼執行之前存在著編譯宣告提升(hoisting)的過程,本例中假設程式碼是已經經過宣告提升過程之後的程式碼

【執行環境棧】

  執行環境棧類似於作用域鏈,有序地儲存著當前程式中存在的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。javascript程式中的執行流正是由這個機制控制著

  在例子中,當執行流進入bar(20)函式時,當前程式的執行環境棧如下圖所示,其中黃色的bar(20)執行環境表示當前程式正處此執行環境中

  當bar(20)函式執行完成後,當前程式的執行環境棧如下圖所示,bar(20)函式的執行環境被銷燬,等待垃圾回收,控制權交還給黃色背景的fn(0)執行環境

說明

  下面按照程式碼執行流的順序對該圖示進行詳細說明

  【1】程式碼執行流進入全域性執行環境,並對全域性執行環境中的程式碼進入宣告提升(hoisting)

  【2】執行流執行第1行程式碼var a = 1;,對a進行LHS查詢,給a賦值1;執行流執行第2行程式碼var b = 2;,對b進行LHS查詢,給b賦值2   【3】執行流執行第15行程式碼fn(0);,呼叫fn(0)函式,此時執行流進入fn(0)函式執行環境中,對該執行環境中的程式碼進行宣告提升過程,並將實參0賦值給形參x中。此時執行環境棧中存在兩個執行環境,fn(0)函式為當前執行流所在執行環境   【4】執行流執行第5行程式碼var a = 10;,對a進行LHS查詢,給a賦值10   【5】執行流執行第12行程式碼bar(20);,呼叫bar(20)函式,此時執行流進入bar(20)函式執行環境中,對該執行環境中的程式碼進行宣告提升過程,並將實參20賦值給形參x中。此時執行環境棧中存在三個執行環境,bar(20)函式為當前執行流所在執行環境   在宣告提升的過程中,由於b是個自由變數,需要通過bar()函式的作用域鏈bar() -> fn() -> 全域性作用域進行查詢,最終在全域性作用域中也就是程式碼第2行找到var b = 2;,然後在全域性執行環境中找到b的值是2,所以給b賦值2   【6】執行流執行第8行程式碼var a = 100;,給a賦值100;執行流執行第9行b = x + a;,對x進行RHS查詢,找到x的值是20,對a進行RHS查詢,找到a的值是100,所以通過計算b的值是120,給b賦值120;執行第10行程式碼return b;,對b進行RHS查詢,找到b的值是120,所以函式返回值為120   【7】執行流執行完第10行程式碼後,bar(20)的執行環境被彈出執行環境棧,並被銷燬,等待垃圾回收,控制權交還給fn(0)函式的執行環境   【8】執行流執行第13行程式碼bar(200);,呼叫bar(200)函式,此時執行流進入bar(200)函式執行環境中,對該執行環境中的程式碼進行宣告提升過程,並將實參200賦值給形參x中。此時執行環境棧中存在三個執行環境,bar(200)函式為當前執行流所在執行環境   與第5步相同,在宣告提升的過程中,由於b是個自由變數,需要通過bar()函式的作用域鏈bar() -> fn() -> 全域性作用域進行查詢,最終在全域性作用域中也就是程式碼第2行找到更新後的var b = 120,然後在全域性執行環境中找到b的值是120,所以給b賦值120   【9】與第6步相同,執行流執行第8行程式碼var a = 100;,給a賦值100;執行流執行第9行b = x + a;,對x進行RHS查詢,找到x的值是200,對a進行RHS查詢,找到a的值是100,所以通過計算b的值是300,給b賦值300;執行第10行程式碼return b;,對b進行RHS查詢,找到b的值是300,所以函式返回值為300   【10】執行流執行完第10行程式碼後,bar(200)的執行環境被彈出執行環境棧,並被銷燬,等待垃圾回收,控制權交還給fn(0)函式的執行環境   【11】執行流執行第14行程式碼},fn(0)的執行環境被彈出執行環境棧,並被銷燬,等待垃圾回收,控制權交還給全域性執行環境   【12】當頁面關閉時,全域性執行環境被銷燬,頁面再無執行環境

總結

  【1】javascript使用的是詞法作用域。對於函式來說,詞法作用域是在函式定義時就已經確定了,與函式是否被呼叫無關。通過作用域,可以知道作用域範圍內的變數和函式有哪些,卻不知道變數的值是什麼。所以作用域是靜態的

  【2】對於函式來說,執行環境是在函式呼叫時確定的,執行環境包含作用域內所有變數和函式的值。在同一作用域下,不同的呼叫(如傳遞不同的引數)會產生不同的執行環境,從而產生不同的變數的值。所以執行環境是動態的