執行環境
執行環境定義了變量和函數有權訪問的其他數據,決定了他們各自的行為。
每個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存在這個對象上,雖然我們在編寫代碼的時候無法訪問這個對象,但解析器在處理數據時會在后台用到它。
ECMAScript實現的宿主環境不同,表示的執行環境對象也不一樣。
在javascript中,可執行的JavaScript代碼分三種類型:
1. Global Code,即全局的、不在任何函數里面的代碼,例如:一個js文件、嵌入在HTML頁面中的js代碼等。
2. Eval Code,即使用eval()函數動態執行的JS代碼。
3. Function Code,即用戶自定義函數中的函數體JS代碼。
不同類型的JavaScript代碼具有不同的執行環境,這里我們不考慮evel code,對應於global code和function code存在2種執行環境:全局執行環境和函數執行環境。
全局執行環境
在一個頁面中,第一次載入JS代碼時創建一個全局執行環境,全局執行環境是最外圍的執行環境,在Web瀏覽器中,全局執行環境被認為是window對象。因此,所有的全局變量和函數都是作為window對象的屬性和方法創建的。
某個執行環境中的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量和函數定義也隨之銷毀,全局執行環境直到應用程序退出后---例如關閉瀏覽器和網頁---時才被銷毀。
函數執行環境
每個函數都有自己的執行環境,當執行進入一個函數時,函數的執行環境就會被推入一個執行環境棧的頂部並獲取執行權。當這個函數執行完畢,它的執行環境又從這個棧的頂部被刪除,並把執行權並還給之前執行環境。這就是ECMAScript程序中的執行流。
【定義期】
函數定義的時候,都會創建一個[[scope]]屬性,這個對象對應的是一個對象的列表,列表中的對象僅能javascript內部訪問,沒法通過語法訪問。
我們定義一全局函數A,那么A函數就創建了一個A的[[scope]]屬性。此時,[[scope]]里面只包含了全局對象【Global Object】。而如果, 我們在A的內部定義一個B函數,那B函數同樣會創建一個[[scope]]屬性,B的[[scope]]屬性包含了兩個對象,一個是A的活動對象【Activation Object】【對於函數來說】一個是全局對象,A的活動對象上面,全局對象在下面。以此類摧,每個函數的都在定義的時候創建自己的[[scope]],里面保存着一個類似於棧的格式的數據。
下面是示例代碼:
// 外部函數 function A(){ var somevar; // 內部函數 function B(){ var somevar; } }
看下示意圖:
【執行期】
當函數被執行的時候,就是進入這個函數的執行環境,首先會創一個它自己的活動對象【Activation Object】(這個對象中包含了this、參數(arguments)、局部變量(包括命名的參數)的定義,當然全局對象是沒有arguments的)和一個變量對象的作用域鏈[[scope chain]],然后,把這個執行環境的[scope]按順序復制到[[scope chain]]里,最后把這個活動對象推入到[[scope chain]]的頂部。這樣[[scope chain]]就是一個有序的棧,這樣保了對執行環境有權訪問的所有變量和對象的有序訪問。
// 第一步頁面載入創全局執行環境global executing context和全局活動象 // 定義全局[[scope]],只含有Window對象 // 掃描全局的定義變量及函數對象:color【undefined】、changecolor【FD創建changecolor的[[scope]],此時里面只含有全局活動對象】,加入到window中,所以全局變量和全局函數對象都是做為window的屬性定義的。 // FD已經定義好所以在此執行環境內任何位置都可以執行changecolor(),color也已經被定義,但是它的值是undefined // 第二步color賦值"blue" var color = "blue"; // 它是不需要賦值的,它就是引用本身 function changecolor() { // 第四步進入changecolor的執行環境 // 復制changecolor的[[scope]]到scope chain // 創建活動對象,掃描定義變量和定義函數,anothercolor【undefined】和swapcolors【FD創建swapcolors的[[scope]]加入changecolor的活動對象和全局活動對象】加入到活動對象,活動對象中同時還要加入arguments和this // 活動對象推入scope chain 頂端 // FD已經定義好所以在此執行環境內任何位置都可以執行swapcolors(),anothercolor也已經被定義好,但它的值是undefined // 第五anothercolor賦值"red" var anothercolor = "red"; // 它是不需要賦值的,它就是引用本身 function swapcolors() { // 第七步進入swapcolors的執行環境,創建它的活動對象 // 復制swapcolors的[[scope]]到scope chain // 掃描定義變量和定義函數對象,活動對象中加入變量tempcolor【undefined】以及arguments和this // 活動對象推入scope chain 頂端 // 第八步tempcolor賦值anothercolor,anothercolor和color會沿着scope chain被查到,並繼續往下執行 var tempcolor = anothercolor; anothercolor = color; color = tempcolor; } // 第六步執行swapcolors,進入其執行環境 swapcolors(); } // 第三步執行changecolor,進入其執行環境 changecolor();
訪問標識符:當執行js代碼的過程中,遇到一個標識符,就會根據標識符的名稱,在執行上下文(Execution Context)的作用域鏈中進行搜索。從作用域鏈的第一個對象(該函數的Activation Object對象)開始,如果沒有找到,就搜索作用域鏈中的下一個對象,如此往復,直到找到了標識符的定義。如果在搜索完作用域中的最后一個對象,也就是全局對象(Global Object)以后也沒有找到,則會拋出一個錯誤,提示undefined。
既然講到執行期,順便講一下javascript的【聲明提升機制Hoisting】
先看下面代碼:
var myvar = 'my value'; alert(myvar); // my value
當然會彈出my value
再看下面這樣
var myvar = 'my value'; (function(){ alert(myvar); // undefined var myvar = 'you value'; })()
結果卻是undefined
這是因為,javascript解析器,進入一個函數執行環境,先對var 和 function進行掃描。相當於會把var或者function【函數聲明】聲明提升到執行環境頂部。
上面的代碼會被解析成下面這樣:
var myvar = 'my value'; (function(){ var myvar; alert(myvar); // undefined myvar = 'you value'; })()
根據,標識符找查機制當執行到alert的時候,查找myvar是局部變量的myvar,此時myvar並沒有被賦值。所以結果是undefined。
再看看下面的例子:
count(1,2); // 3 function count(a,b) { alert(a+b); }
count(1,2); // 會報錯誤count is not a function var count = function (a,b) { alert(a+b); }
我們知道,根據聲明提升機制,var和function都會被提升到執行環境的頂部,已經掃描完畢。所以,上面那種寫法,會把function聲明提到執行環境頂部,所以即使調用在聲明的前面依然能夠執行。
而下面這種寫法會被解析成這樣:
var count; count(1,2); // 會報錯誤count is not a function count = function (a,b) { alert(a+b); }
因為此時count只是被掃描,但還末被引用到函數對象,所以此時,它不是一個function,所以把它當函數來執行會報錯。
另外一個需要提一下的是,函數的聲明是優先於變量的聲明的。
作用域
由上,可以得知,當查找一個個標識符的時候,是在作用域鏈[[scope chain]]里查找的,這個作用域鏈里只包含上一級的活動對象,但並不包括下一級的活動對象。這就意味着,內部執行環境可以訪問外部的執行環境定義的變量,但外部的執行環境是無法訪問內部執行環境定義的變量的。並且,內部環境定義的變量會覆蓋掉外部環境定義的變量。我們還可以知道,特定執行環境的查找的標識符是在【定義期】就已經完成的,而不是在【執行期】,因為作用域鏈[[scope chain]]是從定義期的[scope]復制過來的。但是它本身的Activation Object對象是在執行期才推入,作用域鏈頂端的,所以本身的私有變量是【執行期】完成的。
還是上面的例子:
// color全局變量changecolor 和 swapcolors都可以沿着scope chain訪問到 var color = "blue"; function changecolor() { // anothercolor局部變量,全局訪問不到 但 changecolor和swapcolors都可以沿着scope chain訪問到 var anothercolor = "red"; function swapcolors() { // tempcolo局部變量,全局和changecolor訪問不到 但 swapcolors都可以沿着scope chain訪問到 var tempcolor = anothercolor; anothercolor = color; color = tempcolor; } swapcolors(); } changecolor();
沒有塊級作用域
與C、C++以及JAVA不同,Javscript沒有塊級作用域。看下面代碼:
if(true){ var myvar = "木乙"; } alert(myvar);// 木乙
根據上面我們討論的,如果有塊級作用域,外部是訪問不到myvar的。再看下面
for (var i=0;i<5;i++){ console.log(i) } alert(i); // 5
對於有塊級作用域的語言來說,i做為for初始化的變量,在for之外是訪問不到的,這允分證明了,javascript是沒有塊級作用域的。
延長作用域鏈
雖然執行環境只有兩種——全局作用域和函數作用域,但是還是可以通過某種方式來延長作用域鏈。因為有些語句可以在作用域鏈的頂部增加一個臨時的變量對象。(function bildUrl(){ var qs = "?debug=true"; with(location){ var url = href + qs; } alert(url) })()
with會把location對象的所有屬性和方法包含到變量對象中,並加入到作用域鏈的頂部。此時訪問href實際上就是location.href。
但新聲明的“url”變量會加入到最近的執行環境的變量對象里。試下面例子,說明url是可以訪問到的,只是此時是undefined
(function bildUrl(){ var qs = "?debug=true"; if (!url) { alert("這里可以看到url"); //可以正常顯示url已經被聲明並且被掃描進函數的變量對象 } try{ if (!a) { alert(1);// 報錯因為a根本就不存在 } } catch(e){ console.log("作用域鏈被延長了吧"); } with(location){ var url = href + qs; } })()
對catch語句來說,會創建一個新的變量對象加入到作用域鏈的頂部,其中包含的是被拋出的錯誤對象的聲明。
需要說明的是,ie8之前的版本有個不符合標准的地方,就是被勢出的錯誤對象會被加入到執行環境的變量對象。