JavaScript執行環境及作用域

wrfan發表於2019-04-03

執行環境(有時也稱“環境”)是JavaScript中最為重要的一個概念。執行環境定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。每個執行環境都有一個 與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。雖然我們 編寫的程式碼無法訪問這個物件,但解析器在處理資料時會在後臺使用它。

執行環境

全域性執行環境是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的物件也不一樣。在Web瀏覽器中,全域性執行環境被認為是window物件,因此所有全域性變數和函式都是作為window物件的屬性和方法建立的。某個執行環境中的所有程式碼執行完畢後,該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬(全域性執行環境直到應用程式退出——例如關閉網頁或瀏覽器——時才會被銷燬)。

每個函式都有自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程式中的執行流正是由這個方便的機制控制著。

作用域

當程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈。作用域鏈的用途,是 保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域鏈的前端,始終都是當前執行的程式碼所在環境的變數物件。如果這個環境是函式,則將其活動物件作為變數物件。

活動物件在最開始時只包含一個變數,即arguments物件(這個物件在全域性環境中是不存在的)。作用域鏈中的下一個變數物件來自包含(外部)環境,而再下一個變數物件則來自下一個包含環境。這樣,一直延續到全域性執行環境;全域性執行環境的變數物件始終都是作用域鏈中的最後一個物件。

識別符號解析是沿著作用域鏈一級一級地搜尋識別符號的過程。搜尋過程始終從作用域鏈的前端開始, 然後逐級地向後回溯,直至找到識別符號為止(如果找不到識別符號,通常會導致錯誤發生)。

var color = "blue"; 
function changeColor(){ 
  if (color === "blue"){ 
  color = "red"; 
 } else { 
  color = "blue"; 
 } 
} 
changeColor(); 
alert("Color is now " + color);
複製程式碼

在這個簡單的例子中,函式changeColor()的作用域鏈包含兩個物件:它自己的變數物件(其中定義著arguments物件)和全域性環境的變數物件。可以在函式內部訪問變數 color,就是因為可以在這個作用域鏈中找到它。

延長作用域鏈

雖然執行環境的型別總共只有兩種——全域性和區域性(函式),但還是有其他辦法來延長作用域鏈。這麼說是因為有些語句可以在作用域鏈的前端臨時增加一個變數物件,該變數物件會在程式碼執行後被移除。在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鏈就會得到加長:

  • try-catch 語句的 catch 塊;
  • with 語句

這兩個語句都會在作用域鏈的前端新增一個變數物件。對with語句來說,會將指定的物件新增到作用域鏈中。對catch語句來說,會建立一個新的變數物件,其中包含的是被丟擲的錯誤物件的宣告。下面看一個例子。

function buildUrl() { 
 var qs = "?debug=true"; 
 with(location){ 
 var url = href + qs; 
 } 
 return url; 
}
複製程式碼

在此,with語句接收的是location物件,因此其變數物件中就包含了location 物件的所有屬性和方法,而這個變數物件被新增到了作用域鏈的前端。buildUrl()函式中定義了一個變數qs。當在with語句中引用變數href時(實際引用的是location.href),可以在當前執行環境的變數物件中找到。當引用變數qs時,引用的則是在buildUrl()中定義的那個變數,而該變數位於函式環境的變數物件中。至於with語句內部,則定義了一個名為url的變數,因而url就成了函式執行環境的一部分,所以可以作為函式的值被返回。

沒有塊級作用域

JavaScript沒有塊級作用域經常會導致理解上的困惑。在其他類C的語言中,由花括號封閉的程式碼塊都有自己的作用域(如果用ECMAScript的話來講,就是它們自己的執行環境),因而支援根據條件來定義變數。例如,下面的程式碼在JavaScript中並不會得到想象中的結果:

if (true) { 
 var color = "blue"; 
} 
alert(color); //"blue"
複製程式碼

這裡是在一個if語句中定義了變數color。如果是在CC++Java中,color會在if語句執行完畢後被銷燬。但在JavaScript中,if語句中的變數宣告會將變數新增到當前的執行環境(在這裡是全域性環境)中。在使用for語句時尤其要牢記這一差異,例如:

for (var i=0; i < 10; i++){ 
 doSomething(i); 
} 
alert(i); //10
複製程式碼

對於有塊級作用域的語言來說,for語句初始化變數的表示式所定義的變數,只會存在於迴圈的環境之中。而對於JavaScript來說,由for語句建立的變數i即使在for迴圈執行結束後,也依舊會存在於迴圈外部的執行環境中。

宣告變數

使用var宣告的變數會自動被新增到最接近的環境中。在函式內部,最接近的環境就函式的區域性環境;在with語句中,最接近的環境是函式環境。如果初始化變數時沒有使用var宣告,該變數會自動被新增到全域性環境。如下所示:

function add(num1, num2) { 
 var sum = num1 + num2; 
 return sum; 
} 
var result = add(10, 20); //30 
alert(sum); //由於 sum 不是有效的變數,因此會導致錯誤
複製程式碼

以上程式碼中的函式 add()定義了一個名為sum的區域性變數,該變數包含加法操作的結果。雖然結果值從函式中返回了,但變數sum在函式外部是訪問不到的。如果省略這個例子中的var關鍵字,那麼當add()執行完畢後,sum也將可以訪問到:

function add(num1, num2) { 
 sum = num1 + num2; 
 return sum; 
} 
var result = add(10, 20); //30 
alert(sum); //30
複製程式碼

這個例子中的變數sum在被初始化賦值時沒有使用var關鍵字。於是,當呼叫完add()之後,新增到全域性環境中的變數sum將繼續存在;即使函式已經執行完畢,後面的程式碼依舊可以訪問它。

查詢識別符號

當在某個環境中為了讀取或寫入而引用一個識別符號時,必須通過搜尋來確定該識別符號實際代表什麼。搜尋過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的識別符號。如果在區域性環境中找到了該識別符號,搜尋過程停止,變數就緒。如果在區域性環境中沒有找到該變數名,則繼續沿作用域鏈向上搜尋。搜尋過程將一直追溯到全域性環境的變數物件。如果在全域性環境中也沒有找到這個識別符號,則意味著該變數尚未宣告。

變數查詢也不是沒有代價的。很明顯,訪問區域性變數要比訪問全域性變數更快,因 為不用向上搜尋作用域鏈。JavaScript 引擎在優化識別符號查詢方面做得不錯,因此這 個差別在將來恐怕就可以忽略不計了。

相關文章