4.2 執行環境及做用域【JavaScript高級程序設計第三版】

執行環境(execution context,爲簡單起見,有時也稱爲「環境」)是JavaScript 中最爲重要的一個概念。執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。前端

全局執行環境是最外圍的一個執行環境。根據ECMAScript 實現所在的宿主環境不一樣,表示執行環境的對象也不同。在Web 瀏覽器中,全局執行環境被認爲是window 對象(第7 章將詳細討論),所以全部全局變量和函數都是做爲window 對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。api

每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。ECMAScript 程序中的執行流正是由這個方便的機制控制着。瀏覽器

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即arguments 對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。函數

標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)。優化

請看下面的示例代碼:ui

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

運行一下url

在這個簡單的例子中,函數changeColor()的做用域鏈包含兩個對象:它本身的變量對象(其中定義着arguments 對象)和全局環境的變量對象。能夠在函數內部訪問變量color,就是由於能夠在這個做用域鏈中找到它。debug

此外,在局部做用域中定義的變量能夠在局部環境中與全局變量互換使用,以下面這個例子所示:對象

var color = "blue";
function changeColor() {
	var anotherColor = "red";
	function swapColors() {
		var tempColor = anotherColor;
		anotherColor = color;
		color = tempColor;
		// 這裏能夠訪問color、anotherColor 和tempColor
	}
	// 這裏能夠訪問color 和anotherColor,但不能訪問tempColor
	swapColors();
}
// 這裏只能訪問color
changeColor();

以上代碼共涉及3 個執行環境:全局環境、changeColor()的局部環境和swapColors()的局部環境。全局環境中有一個變量color 和一個函數changeColor()。changeColor()的局部環境中有一個名爲anotherColor 的變量和一個名爲swapColors()的函數,但它也能夠訪問全局環境中的變量color。swapColors()的局部環境中有一個變量tempColor,該變量只能在這個環境中訪問到。blog

不管全局環境仍是changeColor()的局部環境都無權訪問tempColor。然而,在swapColors()內部則能夠訪問其餘兩個環境中的全部變量,由於那兩個環境是它的父執行環境。圖4-3 形象地展現了前面這個例子的做用域鏈。

圖4-3 中的矩形表示特定的執行環境。其中,內部環境能夠經過做用域鏈訪問全部的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。這些環境之間的聯繫是線性、有次序的。每一個環境均可以向上搜索做用域鏈,以查詢變量和函數名;但任何環境都不能經過向下搜索做用域鏈而進入另外一個執行環境。對於這個例子中的swapColors()而言,其做用域鏈中包含3 個對象:swapColors()的變量對象、changeColor()的變量對象和全局變量對象。swapColors()的局部環境開始時會先在本身的變量對象中搜索變量和函數名,若是搜索不到則再搜索上一級做用域鏈。changeColor()的做用域鏈中只包含兩個對象:它本身的變量對象和全局變量對象。這也就是說,它不能訪問swapColors()的環境。

 

函數參數也被看成變量來對待,所以其訪問規則與執行環境中的其餘變量相同。

 

4.2.1 延長做用域鏈

雖然執行環境的類型總共只有兩種——全局和局部(函數),但仍是有其餘辦法來延長做用域鏈。

這麼說是由於有些語句能夠在做用域鏈的前端臨時增長一個變量對象,該變量對象會在代碼執行後被移除。在兩種狀況下會發生這種現象。具體來講,就是當執行流進入下列任何一個語句時,做用域鏈就會獲得加長:

  • 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 就成了函數執行環境的一部分,因此能夠做爲函數的值被返回。

 

在IE8 及以前版本的JavaScript 實現中,存在一個與標準不一致的地方,即在catch 語句中捕獲的錯誤對象會被添加到執行環境的變量對象,而不是catch 語句的變量對象中。換句話說,即便是在catch 塊的外部也能夠訪問到錯誤對象。IE9 修復了這個問題。

 

4.2.2 沒有塊級做用域

JavaScript 沒有塊級做用域常常會致使理解上的困惑。在其餘類C 的語言中,由花括號封閉的代碼塊都有本身的做用域(若是用ECMAScript 的話來說,就是它們本身的執行環境),於是支持根據條件來定義變量。例如,下面的代碼在JavaScript 中並不會獲得想象中的結果:

if (true) {
    var color = "blue";
}
alert(color); //"blue"

這裏是在一個if 語句中定義了變量color。若是是在C、C++或Java 中,color 會在if 語句執行完畢後被銷燬。但在JavaScript 中,if 語句中的變量聲明會將變量添加到當前的執行環境(在這裏是全局環境)中。在使用for 語句時尤爲要牢記這一差別,例如:

for (var i=0; i < 10; i++){
    doSomething(i);
}
alert(i); //10

對於有塊級做用域的語言來講,for 語句初始化變量的表達式所定義的變量,只會存在於循環的環境之中。而對於JavaScript 來講,由for 語句建立的變量i 即便在for 循環執行結束後,也依舊會存在於循環外部的執行環境中。

1. 聲明變量

使用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 代碼的過程當中,不聲明而直接初始化變量是一個常見的錯誤作法,由於這樣可能會致使意外。咱們建議在初始化變量以前,必定要先聲明,這樣就能夠避免相似問題。在嚴格模式下,初始化未經聲明的變量會致使錯誤。

2. 查詢標識符

當在某個環境中爲了讀取或寫入而引用一個標識符時,必須經過搜索來肯定該標識符實際表明什麼。搜索過程從做用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。若是在局部環境中找到了該標識符,搜索過程中止,變量就緒。若是在局部環境中沒有找到該變量名,則繼續沿做用域鏈向上搜索。搜索過程將一直追溯到全局環境的變量對象。若是在全局環境中也沒有找到這個標識符,則意味着該變量還沒有聲明。

經過下面這個示例,能夠理解查詢標識符的過程:

var color = "blue";
    function getColor(){
    return color;
}
alert(getColor()); //"blue"

運行一下

調用本例中的函數getColor()時會引用變量color。爲了肯定變量color 的值,將開始一個兩步的搜索過程。首先,搜索getColor()的變量對象,查找其中是否包含一個名爲color 的標識符。

在沒有找到的狀況下,搜索繼續到下一個變量對象(全局環境的變量對象),而後在那裏找到了名爲color 的標識符。由於搜索到了定義這個變量的變量對象,搜索過程宣告結束。圖4-4 形象地展現了上述搜索過程。

在這個搜索過程當中,若是存在一個局部的變量的定義,則搜索會自動中止,再也不進入另外一個變量對象。換句話說,若是局部環境中存在着同名標識符,就不會使用位於父環境中的標識符,以下面的例子所示:

var color = "blue";
function getColor(){
    var color = "red";
    return color;
}
alert(getColor()); //"red"

運行一下

修改後的代碼在getColor()函數中聲明瞭一個名爲color 的局部變量。調用函數時,該變量就會被聲明。而當函數中的第二行代碼執行時,意味着必須找到並返回變量color 的值。搜索過程首先從局部環境中開始,並且在這裏發現了一個名爲color 的變量,其值爲"red"。由於變量已經找到了,因此搜索即行中止,return 語句就使用這個局部變量,併爲函數會返回"red"。也就是說,任何位於局部變量color 的聲明以後的代碼,若是不使用window.color 都沒法訪問全局color變量。

變量查詢也不是沒有代價的。很明顯,訪問局部變量要比訪問全局變量更快,由於不用向上搜索做用域鏈。JavaScript 引擎在優化標識符查詢方面作得不錯,所以這個差異在未來恐怕就能夠忽略不計了。

 

下載離線版:http://www.shouce.ren/api/view/a/15255

相關文章
相關標籤/搜索