淺談JavaScript 的運行機理
——hechangmin@gmail.com 2010.10
這個話題看似簡單,其實筆者是幾次三番的下筆,又幾次三番的放棄。因為這個內容,對于很多JavaScript的開發人員來講都是一知半解的,當然筆者也在其中,今天之所以出來獻丑了,首先是有了更深的認識,其次微博上有人說“獻丑是進步”,如果獻丑那必定是有同道之人能指出紕漏,那對于筆者本人來講何嘗不是進步呢?深表贊同!
今天會以幾個小小的實例來解讀這個課題。希望能與大家共勉。
首先得先了解JavaScript執行起來的流程,筆者先簡單畫了一個javascript的執行流程圖:

重點解釋的有三步:詞法分析、預解析、執行。
script代碼段:用script標簽分隔的js代碼或引入的js文件。
(1). 預解析
我們先從幾個常見的javascript 小題目入手,請大家先看看下面的范例輸出什么?
<script type="text/javascript">
alert(i); // ?
var i = 1;
</script>
對于javascript的從業者可以試著運行下。看看你的答案和實際輸出一致嗎?別小看這樣兩行腳本,這樣的題目被當作JavaScript的筆試或者面試題目是常有的事情。
實際輸出結果為:“undefined”,
這種現象被稱成“預解析”:JavaScript腳本引擎優先解析var變量和function定義。在預解析完成后,才會執行代碼。
由于變量i 是被 var聲明的,而被優先解析。所以可以理解為在 alert(i) 執行時候,程序前面已經有 var i;
所以上面代碼等效解釋為:
<script type="text/javascript">
var i;
alert(i); // 對于被聲明,但未賦值過的i,輸出‘undefined’的結果,是不應該有任何歧義了吧。
i = 1;
</script>
注意:預解析不會報錯,因為他只解析正確的聲明。
(2). 解釋(主要指詞法分析,生成語法樹的過程)
請注意,這里‘解釋’的定義是筆者自己方便理解自己定義的,而這個‘解釋’并不在預解析之后。
我們知道JavaScript是腳本語言,腳本語言是相對于高級編譯型語言而言他是解釋性的。解釋性語言沒有編譯成二進制代碼,但是要進入到運行階段,都應該是會經過詞法分析、語法分析生成語法樹、語義檢查過程,筆者把這個環節叫做“解釋”,如果讀者有更科學的名字記得告訴我。
解釋性語言在生成語法樹后,就可以執行了。(這個跟腳本引擎編譯器有關)
在這個過程中,有語法檢查(比如括號是否匹配),發現無法生成語法樹,則報錯,結束整個代碼塊的解析。
(3) 執行 與 作用域
引入我們的第二個示例代碼:
<script type="text/javascript">
alert(i); // error: i is not defined.
i = 1;
</script>
聽說JavaScript 變量可以直接用,那為什么還報運行時腳本錯誤?—— i 未定義.
執行過程,需要理解JavaScript的作用域機制,JavaScript變量的作用域是在定義時決定而不是執行時決定,也就是說詞法作用域取決于源碼,編譯器通過靜態分析就能確定,因此詞法作用域也叫做靜態作用域(static scope)。但需要注意,with和eval的語義無法僅通過靜態技術實現,實際上,只能說JS的作用域機制非常接近lexical scope.
JS引擎在執行每個函數實例時,都會創建一個執行環境(execution context)。execution context中包含一個調用對象(call object), 調用對象是一個scriptObject結構,用來保存內部變量表varDecls、內嵌函數表funDecls、父級引用列表upvalue等語法分析結構(注意:varDecls和funDecls等信息是在預解析階段就已經得到,并保存在語法樹中。函數實例執行時,會將這些信息從語法樹復制到scriptObject上)。scriptObject是與函數相關的一套靜態系統,與函數實例的生命周期保持一致。
lexical scope是JS的作用域機制,還需要理解它的實現方法,這就是作用域鏈(scope chain)。scope chain是一個name lookup機制,首先在當前執行環境的scriptObject中尋找,沒找到,則順著upvalue到父級scriptObject中尋找,一直lookup到全局調用對象(global object)。
當一個函數實例執行時,會創建或關聯到一個閉包(closure)。 scriptObject用來靜態保存與函數相關的變量表,closure則在執行期動態保存這些變量表及其運行值。closure的生命周期有可能比函數實例長。函數實例在活動引用為空后會自動銷毀,closure則要等要數據引用為空后,由JS引擎回收(有些情況下不會自動回收,就導致了內存泄漏)。
別被上面的一堆名詞嚇住,一旦理解了執行環境、調用對象、閉包、詞法作用域、作用域鏈這些概念,JS語言的很多現象都能迎刃而解。
小結
“預解析”,其實是在的‘解釋’階段完成,并存儲在語法樹中。當執行到函數實例時,會將varDelcs和funcDecls從語法樹中復制到執行環境的scriptObject上。
對于示例解析:
未定義變量意味著在scriptObject的變量表中找不到,JS引擎會沿著scriptObject的upvalue往上尋找,如果都沒找到,對于寫操作i = 1; 最后就會等價為 window.i = 1; 給window對象新增了一個屬性。對于讀操作,如果一直追溯到全局執行環境的scriptObject上都找不到,就會產生運行期錯誤。
最后,留個問題給大家:
<script type="text/javascript">
var arg = 1;
function foo(arg) {
alert(arg);
var arg = 2;
}
foo(3);
</script>
請問alert的輸出是什么?