JS語法作用域與詞法作用域

mybwu_com發表於2013-12-28

原文地址:http://blog.csdn.net/huli870715/article/details/6387243

<script type="text/javascript">
var ClassA = function(){
	this.prop1 = 1;
};
ClassA.prototype.func1 = function(){
	var that = this,
	    var1 = 2;
	    
	function a(){
		return function(){
			alert(var1);
			alert(this.prop1);
		}.apply(that);
	}
	a();
};
var objA = new ClassA();
objA.func1();
</script>
大家應該寫過上面類似的程式碼吧,其實這裡我想要表達的是有時候一個方法定義的地方和使用的地方會相隔十萬八千里,那方法執行時,它能訪問哪些變數,不能訪問哪些變數,這個怎麼判斷呢?這個就是我們這次需要分析的問題—詞法作用域。

詞法作用域:變數的作用域是在定義時決定而不是執行時決定,也就是說詞法作用域取決於原始碼,通過靜態分析就能確定,因此詞法作用域也叫做靜態作用域。 with和eval除外,所以只能說JS的作用域機制非常接近詞法作用域(Lexical scope)。

下面通過幾個小小的案例,開始深入的瞭解對理解詞法作用域和閉包必不可少的,JS執行時底層的一些概念和理論知識。

經典案列重現

1、經典案例一

<script type="text/javascript">
function a(i){
	var i;
	alert(i);
}
a(10);
</script>

疑問:上面的程式碼會輸出什麼呢?
答案:沒錯,就是彈出10。具體執行過程應該是這樣的

  1. a 函式有一個形參 i,呼叫 a 函式時傳入實參 10,形參 i=10
  2. 接著定義一個同名的區域性變數 i,未賦值
  3. alert 輸出 10
  4. 思考:區域性變數 i 和形參 i 是同一個儲存空間嗎?

2、經典案例二

<script type="text/javascript">
function a(i){
	alert(i);
	alert(arguments[0]); // arguments[0] 應該就是形參 i
	var i = 2;
	alert(i);
	alert(arguments[0]); 
}
a(10);
</script>

疑問:上面的程式碼又會輸出什麼呢?(( 10,10,2,10 || 10,10,2,2 ))
答案:在FireBug中的執行結果是第二個10,10,2,2,猜對了… ,下面簡單說一下具體執行過程

  1. a 函式有一個形參i,呼叫 a 函式時傳入實參 10,形參 i=10
  2. 第一個 alert 把形參 i 的值 10 輸出
  3. 第二個 alert 把 arguments[0] 輸出,應該也是 i
  4. 接著定義個區域性變數 i 並賦值為2,這時候區域性變數 i=2
  5. 第三個 alert 就把區域性變數 i 的值 2 輸出
  6. 第四個alert再次把 arguments[0] 輸出
  7. 思考:這裡能說明區域性變數 i 和形參 i 的值相同嗎?

3、經典案例三

<script type="text/javascript">

function a(i){
var i = i;
alert(i);
}
a(10);
</script>

疑問:上面的程式碼又又會輸出什麼呢?(( undefined || 10 ))
答案:在FireBug中的執行結果是 10,下面簡單說一下具體執行過程

  1. 第一句宣告一個與形參 i 同名的區域性變數 i,根據結果我們知道,後一個 i 是指向了形參 i,所以這裡就等於把形參 i 的值 10 賦了區域性變數 i
  2. 第二個 alert 當然就輸出 10
  3. 思考:結合案列二,這裡基本能說明區域性變數 i 和形參 i 指向了同一個儲存地址!

4、經典案例四

<script type="text/javascript">
var i = 10;
function a(){
	alert(i);
	var i = 2;
	alert(i);
}
a();
</script>

疑問:上面的程式碼又會輸出什麼呢?(小子,看這回整不死你!哇哈哈,就不給你選項)
答案:在FireBug中的執行結果是 undefined, 2,下面簡單說一下具體執行過程

  1. 第一個alert輸出undefined
  2. 第二個alert輸出 2
  3. 思考:到底怎麼回事兒?

5、經典案例五…………..N

看到上面的幾個例子,你可能會想,怎麼可能,我寫了幾年的 js 了,怎麼這麼簡單例子也會猶豫,結果可能還答錯了。其實可能原因是:我們能很快的寫出一個方法,但到底方法內部是怎麼執行的呢?執行的細節又是怎麼樣的呢?你可能沒有進行過深入的學習和了解。要了解這些細節,那就需要了解 JS 引擎的工作方式,所以下面我們就把 JS 引擎對一個方法的解析過程進行一個稍微深入一些的介紹

解析過程

1、執行順序

  • 編譯型語言,編譯步驟分為:詞法分析、語法分析、語義檢查、程式碼優化和位元組生成。
  • 解釋型語言,通過詞法分析和語法分析得到語法分析樹後,就可以開始解釋執行了。這裡是一個簡單原始的關於解析過程的原理,僅作為參考,詳細的解析過程(各種JS引擎還有不同)還需要更深一步的研究

JavaScript執行過程,如果一個文件流中包含多個script程式碼段(用script標籤分隔的js程式碼或引入的js檔案),它們的執行順序是:

  1. 步驟1. 讀入第一個程式碼段(js執行引擎並非一行一行地執行程式,而是一段一段地分析執行的)
  2. 步驟2. 做詞法分析和語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到步驟5
  3. 步驟3. 對【var】變數和【function】定義做“預解析“(永遠不會報錯的,因為只解析正確的宣告)
  4. 步驟4. 執行程式碼段,有錯則報錯(比如變數未定義)
  5. 步驟5. 如果還有下一個程式碼段,則讀入下一個程式碼段,重複步驟2
  6. 步驟6. 結束

2、特殊說明
全域性域(window)域下所有JS程式碼可以被看成是一個“匿名方法“,它會被自動執行,而此“匿名方法“內的其它方法則是在被顯示呼叫的時候才被執行
3、關鍵步驟
上面的過程,我們主要是分成兩個階段

  1. 解析:就是通過語法分析和預解析構造合法的語法分析樹。
  2. 執行:執行具體的某個function,JS引擎在執行每個函式例項時,都會建立一個執行環境(ExecutionContext)和活動物件(activeObject)(它們屬於宿主物件,與函式例項的生命週期保持一致)

3、關鍵概念
到這裡,我們再更強調以下一些概念,這些概念都會在下面用一個一個的實體來表示,便於大家理解

  • 語法分析樹(SyntaxTree)可以直觀地表示出這段程式碼的相關資訊,具體的實現就是JS引擎建立了一些表,用來記錄每個方法內的變數集(variables),方法集(functions)和作用域(scope)等
  • 執行環境(ExecutionContext)可理解為一個記錄當前執行的方法【外部描述資訊】的物件,記錄所執行方法的型別,名稱,引數和活動物件(activeObject)
  • 活動物件(activeObject)可理解為一個記錄當前執行的方法【內部執行資訊】的物件,記錄內部變數集(variables)、內嵌函式集(functions)、實參(arguments)、作用域鏈(scopeChain)等執行所需資訊,其中內部變數集(variables)、內嵌函式集(functions)是直接從第一步建立的語法分析樹複製過來的
  • 詞法作用域:變數的作用域是在定義時決定而不是執行時決定,也就是說詞法作用域取決於原始碼,通過靜態分析就能確定,因此詞法作用域也叫做靜態作用域。 with和eval除外,所以只能說JS的作用域機制非常接近詞法作用域(Lexical scope)
  • 作用域鏈:詞法作用域的實現機制就是作用域鏈(scopeChain)。作用域鏈是一套按名稱查詢(Name Lookup)的機制,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著作用域鏈到父 ActiveObject 中尋找,一直找到全域性呼叫物件(Global Object)

4、實體表示


解析模擬

估計,看到這兒,大家還是很朦朧吧,什麼是語法分析樹,語法分析樹到底長什麼樣子,作用域鏈又怎麼實現的,活動物件又有什麼內容等等,還是不是太清晰,下面我們就通過一段實際的程式碼來模擬整個解析過程,我們就把語法分析樹,活動物件實實在在的建立出來,理解作用域,作用域鏈的到底是怎麼實現的
1、模擬程式碼

<script type="text/javascript">
/* 全域性 (window) 域下的一段程式碼 */
var i = 1, j = 2, k = 3;
function a(o, p, x, q){
	var  x = 4;
	alert(i);
	
	function b(r, s){
		var i = 11, y = 5;
		alert(i);
		
		function c(t){
			var z = 6;
			alert(i);
		}
		// 函式表示式
		var d = function(){
			alert(y);
		};
		
		c(60);
		d();
	}
	
	b(40, 50);
}
a(10, 20, 30);
</script>

2、語法分析樹
上面的程式碼很簡單,就是先定義了一些全域性變數和全域性方法,接著在方法內再定義區域性變數和區域性方法,現在JS直譯器讀入這段程式碼開始解析,前面提到 JS 引擎會先通過語法分析和預解析得到語法分析樹,至於語法分析樹長什麼樣兒,都有些什麼資訊,下面我們以一種簡單的結構:一個 JS 物件(為了清晰表示個各種物件間的引用關係,這裡的只是偽物件表示,可能無法執行)來描述語法分析樹(這是我們比較熟悉的,實際結構我們不去深究,肯定複雜得多,這裡是為了幫助理解解析過程而特意簡化)

  1. /**
  2. * 模擬建立一棵語法分析樹,儲存function內的變數和方法
  3. */
  4. varSyntaxTree={
  5. // 全域性物件在語法分析樹中的表示
  6. window:{
  7. variables:{
  8. i:{value:1},
  9. j:{value:2},
  10. k:{value:3}
  11. },
  12. functions:{
  13. a:this.a
  14. }
  15. },
  16. a:{
  17. variables:{
  18. x:'undefined'
  19. },
  20. functions:{
  21. b:this.b
  22. },
  23. scope:this.window
  24. },
  25. b:{
  26. variables:{
  27. y:'undefined'
  28. },
  29. functions:{
  30. c:this.c,
  31. d:this.d
  32. },
  33. scope:this.a
  34. },
  35. c:{
  36. variables:{
  37. z:'undefined'
  38. },
  39. functions:{},
  40. scope:this.b
  41. },
  42. d:{
  43. variables:{},
  44. functions:{},
  45. scope:{
  46. myname:d,
  47. scope:this.b
  48. }
  49. }
  50. };

上面就是關於語法分析樹的一個簡單表示,正如我們前面分析的,語法分析樹主要記錄了每個 function 中的變數集(variables),方法集(functions)和作用域(scope)
語法分析樹關鍵點

  • 1變數集(variables)中,只有變數定義,沒有變數值,這時候的變數值全部為“undefined”
  • 2作用域(scope),根據詞法作用域的特點,這個時候每個變數的作用域就已經明確了,而不會隨執行時的環境而改變。【什麼意思呢?就是我們經常將一個方法 return 回去,然後在另外一個方法中去執行,執行時,方法中變數的作用域是按照方法定義時的作用域走。其實這裡想表達的意思就是不管你在多麼複雜,多麼遠的地方執行該方法,最終判斷方法中變數能否被訪問還是得回到方法定義時的地方查證】
  • 3作用域(scope)建立規則
  • a對於函式宣告和匿名函式表示式來說,[scope]就是它建立時的作用域
  • b對於有名字的函式表示式,[scope]頂端是一個新的JS物件(也就是繼承了Object.prototype),這個物件有兩個屬性,第一個是自身的名稱,第二個是定義的作用域,第一個函式名稱是為了確保函式內部的程式碼可以無誤地訪問自己的函式名進行遞迴。

3、執行環境與活動物件
語法分析完成,開始執行程式碼。我們呼叫每一個方法的時候,JS 引擎都會自動為其建立一個執行環境和一個活動物件,它們和方法例項的生命週期保持一致,為方法執行提供必要的執行支援,針對上面的幾個方法,我們這裡統一為其建立了活動物件(按道理是在執行方法的時候才會生成活動物件,為了便於演示,這裡一下子定義了所有方法的活動物件),具體如下:
執行環境

  1. /**
  2. * 執行環境:函式執行時建立的執行環境
  3. */
  4. varExecutionContext={
  5. window:{
  6. type:'global',
  7. name:'global',
  8. body:ActiveObject.window
  9. },
  10. a:{
  11. type:'function',
  12. name:'a',
  13. body:ActiveObject.a,
  14. scopeChain:this.window.body
  15. },
  16. b:{
  17. type:'function',
  18. name:'b',
  19. body:ActiveObject.b,
  20. scopeChain:this.a.body
  21. },
  22. c:{
  23. type:'function',
  24. name:'c',
  25. body:ActiveObject.c,
  26. scopeChain:this.b.body
  27. },
  28. d:{
  29. type:'function',
  30. name:'d',
  31. body:ActiveObject.d,
  32. scopeChain:this.b.body
  33. }
  34. }

上面每一個方法的執行環境都儲存了相應方法的型別(function)、方法名稱(funcName)、活動物件(ActiveObject)、作用域鏈(scopeChain)等資訊,其關鍵點如下:

  • body屬性,直接指向當前方法的活動物件
  • scopeChain屬性,作用域鏈,它是一個連結串列結構,根據語法分析樹中當前方法對應的scope屬性,它指向scope對應的方法的活動物件(ActivceObject),變數查詢就是跟著這條鏈條查詢的

活動物件

  1. /**
  2. * 活動物件:函式執行時建立的活動物件列表
  3. */
  4. varActiveObject={
  5. window:{
  6. variables:{
  7. i:{value:1},
  8. j:{value:2},
  9. k:{value:3}
  10. },
  11. functions:{
  12. a:this.a
  13. }
  14. },
  15. a:{
  16. variables:{
  17. x:{value:4}
  18. },
  19. functions:{
  20. b:SyntaxTree.b
  21. },
  22. parameters:{
  23. o:{value:10},
  24. p:{value:20},
  25. x:this.variables.x,
  26. q:'undefined'
  27. },
  28. arguments:[this.parameters.o,this.parameters.p,this.parameters.x]
  29. },
  30. b:{
  31. variables:{
  32. y:{value:5}
  33. },
  34. functions:{
  35. c:SyntaxTree.c,
  36. d:SyntaxTree.d
  37. },
  38. parameters:{
  39. r:{value:40},
  40. s:{value:50}
  41. },
  42. arguments:[this.parameters.r,this.parameters.s]
  43. },
  44. c:{
  45. variables:{
  46. z:{value:6}
  47. },
  48. functions:{},
  49. parameters:{
  50. u:{value:70}
  51. },
  52. arguments:[this.parameters.u]
  53. },
  54. d:{
  55. variables:{},
  56. functions:{},
  57. parameters:{},
  58. arguments:[]
  59. }
  60. }

上面每一個活動物件都儲存了相應方法的內部變數集(variables)、內嵌函式集(functions)、形參(parameters)、實參(arguments)等執行所需資訊,活動物件關鍵點

  • 建立活動物件,從語法分析樹複製方法的內部變數集(variables)和內嵌函式集(functions)
  • 方法開始執行,活動物件裡的內部變數集全部被重置為 undefined
  • 建立形參(parameters)和實參(arguments)物件,同名的實參,形參和變數之間是【引用】關係
  • 執行方法內的賦值語句,這才會對變數集中的變數進行賦值處理
  • 變數查詢規則是首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)
  • 方法執行完成後,內部變數值不會被重置,至於變數什麼時候被銷燬,請參考下面一條
  • 方法內變數的生存週期取決於方法例項是否存在活動引用,如沒有就銷燬活動物件
  • 6和7 是使閉包能訪問到外部變數的根本原因

重釋經典案例

案列一二三

根據【在一個方法中,同名的實參,形參和變數之間是引用關係,也就是JS引擎的處理是同名變數和形參都引用同一個記憶體地址】,所以才會有二中的修改arguments會影響到區域性變數的情況出現

案例四

根據【JS引擎變數查詢規則,首先在當前執行環境的 ActiveObject 中尋找,沒找到,則順著執行環境中屬性 ScopeChain 指向的 ActiveObject 中尋找,一直到 Global Object(window)】,所以在四中,因為在當前的ActiveObject中找到了有變數 i 的定義,只是值為 “undefined”,所以直接輸出 “undefined” 了

總結

以上是我在學習和使用了JS一段時間後,為了更深入的瞭解它, 也為了更好的把握對它的應用, 從而在對閉包的學習過程中,自己對於詞法作用域的一些理解和總結,中間可能有一些地方和真實的JS解釋引擎有差異,因為我只是站在一個剛入門的前端開發人員而不是系統設計者的角度上去分析這個問題,希望能對JS開發者理解此法作用域帶來一些幫助!

相關文章