前面的話
對于執行環境(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)












總結
【1】javascript使用的是詞法作用域。對于函數來說,詞法作用域是在函數定義時就已經確定了,與函數是否被調用無關。通過作用域,可以知道作用域范圍內的變量和函數有哪些,卻不知道變量的值是什么。所以作用域是靜態的
[注意]通過eval()函數和with語句可以對作用域進行動態修改
【2】對于函數來說,執行環境是在函數調用時確定的,執行環境包含作用域內所有變量和函數的值。在同一作用域下,不同的調用(如傳遞不同的參數)會產生不同的執行環境,從而產生不同的變量的值。所以執行環境是動態的
文章列表