再談 JavaScript 執行環境與做用域

  前面我說到過,執行環境是js中最爲重要的一個概念。執行環境定義了變量和函數有權訪問的其餘數據,決定了它們各自的行爲。(接下來的概述主要來自《高性能JavaScript》一書,以及本人的一些簡單的理解。)javascript

 

1、函數做用域html

  在JavaScript中,每個函數都表示爲一個對象,更確切地說,是Function對象的一個實例。Function對象與其餘的對象同樣,都擁有能夠編程訪問的屬性,和一系列不能經過代碼訪問而僅提供了JavaScript引擎存取的內部屬性。譬如[[Call]]屬性,表示這個對象能夠被執行,其中有一個內部屬性是[[Scope]],由ECMA-262標準第三版定義。前端

  內部屬性[[Scope]]包含了一個函數被建立的做用域中的對象的集合。這個集合被稱爲函數的做用域鏈,它決定了哪些數據能被函數訪問。函數做用域中的每一個對象都被稱爲可變對象,每一個可變對象都以「鍵值對」的形式存在。當一個函數建立後,它的做用域鏈會被建立此函數的做用域中可訪問的數據對象所填充。java

  上面這句畫的意思就是函數被建立的時候會有一個咱們沒法訪問的[[scope]]屬性,[[scope]]屬性中包含了當前函數能夠訪問的做用域中的全部的對象的集合或者說是列表,而這個集合或者說列表被稱爲函數的做用域鏈,函數只能訪問到這個做用域鏈中的數據。函數的做用域鏈中保存着變量對象,這些變量對象都以「鍵值對」(屬性和值)的形式存儲。當一個函數被建立後,它的做用域鏈中會有一個,保存了當前執行環境中的全部的變量和函數的對象(變量對象)。編程

function sum(num1, num2){
	var num3 = 10;
	return num1 + num2 + num3;
}

  上面的sum()函數建立的時候,他的做用域鏈中插入了一個變量對象,這個變量對象包含了全部全局執行環境中定義的變量或函數。例如window、document、sum等等。以下圖所示:函數

  當sum()函數被調用(執行)的時候會建立一個被稱爲執行環境(execution context)的內部對象。函數每次調用時對應的執行環境都是獨一無二的,因此屢次調用同一個函數就會致使建立多個執行環境。當函數執行完畢時,執行環境就會被銷燬。性能

function sum(num1, num2){
	var num3 = 10;
	return num1 + num2 + num3;
}

var count = sum(5); 

  每一個執行環境都有本身的做用域鏈,用於解析標識符(也就是查詢變量是否存在)。當執行環境被建立時,它的做用域鏈就會被初始化,連同運行函數的[[Scope]]屬性中所包含的對象(如上圖所示)。這些值按照它們出如今函數中的順序,被複制到執行環境的做用域鏈中。這項工做一旦完成,一個被稱做「激活對象」的新對象就爲執行環境建立好了。這個激活對象做爲函數執行期間的一個變量對象,包含訪問全部局部變量,命名參數,參數集合以及this。而後,這個激活對象被推入做用域鏈的前端。看成用域鏈被銷燬時,激活對象也一同被銷燬。以下圖所示。this

  

  上圖顯示了函數在被調用時做用域的變化,關於變量聲明和函數聲明初始化以及函數參數初始化的值以及衝突的解決方案。能夠看我以前的博客。      http://www.cnblogs.com/miracle-t/p/5484420.htmlspa

  在函數執行過程當中,沒遇到一個變量,都會經歷一次標識符解析的過程,以肯定從哪裏獲取或存儲數據。這個過程會搜索執行環境的做用域鏈,查找同名的標識符(變量名)。搜索的過程從做用域的頭部開始,也就是當前運行函數的活動對象。若是找到了,就是用這個標識符對應的變量;若是沒有找到,就繼續搜索做用域鏈中的下一個對象。若是整個做用域鏈中全部對象都沒有該標識符,那麼表示該標識符是未定義的。code

 

2、改變做用域

  通常來講,一個執行環境的做用域鏈是不會發生改變的。可是,有兩個語句能夠在執行時臨時改變做用域鏈。

  第一個語句是with語句,with語句用來給對象的全部屬性建立一個變量。在其餘語言中,相似功能一般用來避免書寫重複代碼。請看下列代碼:

var obj = {name : "MT", age : 24, sex : "men"}

function checkObj(){
	with(obj){
		console.log(name);
		console.log(myName);
		var myName = name,
			myAge = age,
			mySex = sex;
			
			
	}
	console.log(mySex);
	console.log(name);
	console.log(sex);
}

checkObj();

  上面的代碼執行到with語句的時候,執行環境的做用域臨時被改變了。一個新的變量對象被建立,它包含了參數指定的對象的全部屬性。這個對象被推入做用域鏈的頂端,這意味着函數的全部局部變量如今處於做用域鏈中的第二個對象中,所以訪問的代價更高了。以下圖所示:

  經過把obj對象傳遞給with語句,一個包含了全局的obj對象的全部屬性的新的變量對象就被推入到做用域鏈的頭部。這使得訪問obj對象的屬性很是快,而訪問局部變量則變慢了,不如變量myName。所以,最好避免使用with語句。

  在JavaScript中,並非只有with語句能人爲的修改做用域鏈,try-catch語句中的catch子句也具備一樣的效果。當try代碼塊中發生錯誤,執行過程會自動跳轉到catch子句,而後把異常對象推入一個變量對象並置於做用域的首位。在catch代碼塊內部,函數全部局部變量將會放在第二個做用域鏈對象中。請看下列代碼:

try{
	methodThatMightCuseAnError();
}catch(ex){
	alert(ex.message); //做用域鏈在此處改變。
}

  請注意,一旦catch子句執行完畢,做用域鏈就會返回以前的狀態。

 

3、動態做用域

  不管是with語句仍是try-catch語句中的catch子句,或是包含eval()的函數,都被認爲是動態做用域。動態做用域只存在於代碼執行過程當中,所以沒法經過靜態(查看代碼結構)分析檢測出來。例如:

function execute(code){
	eval(code);
	
	function subroutine(){
		return window;
	}
	
	var w = subroutine();
	
	//w是什麼?
}

  因爲使用了eval(),函數execute()看上去像動態做用域。變量w的值會隨着code的值改變。大部分狀況下,w等同於全局的window對象,可是考慮以下狀況:

execute("var window = {}");

  以上diam中,execute()中的eval()建立了一個局部變量window,所以w等同於局部變量window,而非全局window對象。只有執行這段代碼時纔會發現問題,這意味着window標識符的真實值是沒法預知的。

  所以,只有在確實有必要時才推薦使用動態做用域。

相關文章
相關標籤/搜索