標籤:回收 一起 其他 調用 ges 過程 pre function 執行環境
前面的話
對於執行環境(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查詢,則在全域環境中聲明該變數,若是strict 模式下的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 = 2;,然後在全域執行環境中找到b的值是2,所以給b賦值2 【9】與第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 【10】執行流執行完第10行代碼後,bar(200)的執行環境被彈出執行環境棧,並被銷毀,等待記憶體回收,控制權交還給fn(0)函數的執行環境 【11】執行流執行第14行代碼},fn(0)的執行環境被彈出執行環境棧,並被銷毀,等待記憶體回收,控制權交還給全域執行環境 【12】當頁面關閉時,全域執行環境被銷毀,頁面再無執行環境總結
【1】javascript使用的是詞法範圍。對於函數來說,詞法範圍是在函數定義時就已經確定了,與函數是否被調用無關。通過範圍,可以知道範圍範圍內的變數和函數有哪些,卻不知道變數的值是什麼。所以範圍是靜態
[注意]通過eval()函數和with語句可以對範圍進行動態修改
【2】對於函數來說,執行環境是在函數調用時確定的,執行環境包含範圍內所有變數和函數的值。在同一範圍下,不同的調用(如傳遞不同的參數)會產生不同的執行環境,從而產生不同的變數的值。所以執行環境是動態
深入理解javascript範圍系列第五篇