從一個簡單的問題談起:
<script type="text/javascript">
alert(i); // ?
var i = 1;
</script>
輸出結果是undefined, 這種現象稱為「預先解析」:JavaScript引擎會優先解析var變數和function定義。在預解析完成後,才會執行程式碼。如果一個文檔流中包含多個script代碼段(用script標籤分隔的js代碼或引入的js文件),運行順序是:
step1. 讀入第一個代碼段
step2. 做語法分析,有錯則報語法錯誤(例如括號不符等),並跳到step5
step3. 對var變數和function定義做「預解析」(永遠不會報錯的,因為只解析正確的宣告)
step4. 執行程式碼段,有錯則報錯(如變數未定義)
step5. 如果還有下一個程式碼段,則讀入下一個程式碼段,重複step2
step6. 結束上面的分析,已經能解釋很多問題了,但老覺得欠缺點什麼。例如step3裡,「預解析」究竟是怎麼回事?還有step4裡,看下面的範例:
<script type="text/javascript">
alert(i); // error: i is not defined.
i = 1;
</script>
為什麼第一句會導致錯誤? JavaScript中,變數不是可以不定義嗎?
編譯過程時間如白馬過隙,書櫃旁翻開恍如隔世般的《編譯原理》,熟悉而又陌生的空白處有著這樣的筆記:
對於傳統編譯型語言來說,編譯步驟分為:詞法分析、語法分析、語義檢查、程式碼最佳化和位元組生成。
但對於解釋型語言來說,透過詞法分析和文法分析得到語法樹後,就可以開始解釋執行了。
簡單地說,詞法分析是將字元流(char stream)轉換為記號流(token stream), 例如將c = a - b;轉換為:
NAME "c"
EQUALS
NAME "a"
MINUS
NAME "b"
SEMICOLON
上面只是範例,更進一步的了解請看Lexical Analysis.
《JavaScript權威指南》的第2章,講的就是詞法結構(Lexical Structure),ECMA-262 中也有描述。詞法結構是一門語言的基礎,很容易掌握。至於詞法分析的實現那是另一個研究領域,在此不探究。
可以拿自然語言來類比,詞法分析是一對一的硬性翻譯,例如一段英文,逐詞翻譯成中文,得到的是一堆記號流,還很難理解。進一步的翻譯,就需要語法分析了,下圖是一個條件語句的語法樹:
構造語法樹的時候,如果發現無法構造,例如if(a { i = 2; }, 就會報語法錯誤,並結束整個程式碼區塊的解析,這就是本文開頭部分的step2.
透過語法分析,構造出語法樹後,翻譯出來的句子可能還會有模糊的地方,接下來還需要進一步的語意檢查。形參類型是否匹配
。出,對於JavaScript引擎來說,肯定有詞法分析和語法分析,之後可能還有語義檢查、代碼優化等步驟,等這些編譯步驟完成之後(任何語言都有編譯過程,只是解釋型語言沒有編譯成二進制
程式
碼),才會開始執行程式碼
。
與程式設計實踐》的第二部分,對此有非常仔細的
分析
。理解JavaScript的作用域機制,JavaScript採用的是詞法作用域(lexcical scope)。編譯器透過靜態分析就能確定,因此詞法作用域也叫做靜態作用域(static scope)。非常接近lexical scope.
JS引擎在執行每個函數實例時,都會創建一個執行環境(execution context)。 varDecls、內嵌函數表funDecls、父級引用列表upvalue等語法分析結構(注意:varDecls和funDecls等資訊是在語法分析階段就已經得到,並保存在語法樹中。函數實例執行時,會將這些訊息從語法樹複製到scriptObject上)。
lexical scope是JS的作用域機制,也需要理解它的實作方法,這就是作用域鏈(scope chain)。 scope chain是一個name lookup機制,先在目前執行環境的scriptObject中尋找,沒找到,則順著upvalue到父級scriptObject中尋找,一直lookup到全域呼叫物件(global object)。
當一個函數實例執行時,會建立或關聯到一個閉包(closure)。 scriptObject用來靜態保存與函數相關的變數表,而closure則在執行期間動態保存這些變數表及其運行值。 closure的生命週期有可能比函數實例長。函數實例在活動引用為空後會自動銷毀,closure則要等要資料引用為空後,由JS引擎回收(有些情況不會自動回收,就導致了記憶體洩漏)。
別被上面的一堆名詞嚇住,一旦理解了執行環境、呼叫物件、閉包、詞法作用域、作用域鏈這些概念,JS語言的許多現像都能迎刃而解。
小結至此,對於文章開頭部分的疑問,可以解釋得很清楚了:
step3中所謂的“預解析”,其實是在step2的語法分析階段完成,並存儲在語法樹中。當執行到函數實例時,會將varDelcs和funcDecls從語法樹複製到執行環境的scriptObject。
step4中,未定義變數表示在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的輸出是什麼?