從一個簡單的問題談起:
<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 中也有描述。詞法結構是一門語言的基礎,很容易掌握。至于詞法分析的實現那是另一個研究領域,在此不探究。
可以拿自然語言來類比,詞法分析是一對一的硬性翻譯,比如一段英文,逐詞翻譯成中文,得到的是一堆記號流,還很難理解。進一步的翻譯,就需要語法分析了,下圖是一個條件語句的語法樹:
synttree
構造語法樹的時候,如果發現無法構造,比如if(a { i = 2; }, 就會報語法錯誤,并結束整個代碼塊的解析,這就是本文開頭部分的step2.
通過語法分析,構造出語法樹后,翻譯出來的句子可能還會有模糊不清的地方,接下來還需要進一步的語義檢查。對于傳統強類型語言來說,語義檢查的主要部分是類型檢查,比如函數的實參和形參類型是否匹配。對于弱類型語言來說,這一步可能沒有(精力有限,沒時間去看JS的引擎實現,不敢確定JS引擎中是否有語義檢查這一步)。
通過上面的分析可以看出,對于JavaScript引擎來說,肯定有詞法分析和語法分析,之后可能還有語義檢查、代碼優化等步驟,等這些編譯步驟完成之后(任何語言都有編譯過程,只是解釋型語言沒有編譯成二進制代碼),才會開始執行代碼。
上面的編譯過程,還是無法更深入的解釋文章開頭部分的“預解析”,我們還得仔細探究下JavaScript代碼的執行過程。
執行過程
周愛民在《JavaScript語言精髓與編程實踐》的第二部分,對此有非常仔細的分析。下面是我的一些領悟:
通過編譯,JavaScript代碼已經翻譯成了語法樹,然后會立刻按照語法樹執行。
進一步的執行過程,需要理解JavaScript的作用域機制,JavaScript采用的是詞法作用域(lexcical scope)。通俗地講,就是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語言的很多現象都能迎刃而解。
小結
至此,對于文章開頭部分的疑問,可以解釋得很清楚了:
step3中所謂的“預解析”,其實是在step2的語法分析階段完成,并存儲在語法樹中。當執行到函數實例時,會將varDelcs和 funcDecls從語法樹中復制到執行環境的scriptObject上。
step4中,未定義變量意味著在scriptObject的變量表中找不到,JS引擎會沿著scriptObject的upvalue往上尋找,如果都沒找到,對于寫操作i = 1; 最后就會等價為 window.i = 1; 給window對象新增了一個屬性。對于讀操作,如果一直追溯到全局執行環境的scriptObject上都找不到,就會產生運行期錯誤。
理解后,霧散花開,天空一片晴朗。
<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 中也有描述。詞法結構是一門語言的基礎,很容易掌握。至于詞法分析的實現那是另一個研究領域,在此不探究。
可以拿自然語言來類比,詞法分析是一對一的硬性翻譯,比如一段英文,逐詞翻譯成中文,得到的是一堆記號流,還很難理解。進一步的翻譯,就需要語法分析了,下圖是一個條件語句的語法樹:
synttree
構造語法樹的時候,如果發現無法構造,比如if(a { i = 2; }, 就會報語法錯誤,并結束整個代碼塊的解析,這就是本文開頭部分的step2.
通過語法分析,構造出語法樹后,翻譯出來的句子可能還會有模糊不清的地方,接下來還需要進一步的語義檢查。對于傳統強類型語言來說,語義檢查的主要部分是類型檢查,比如函數的實參和形參類型是否匹配。對于弱類型語言來說,這一步可能沒有(精力有限,沒時間去看JS的引擎實現,不敢確定JS引擎中是否有語義檢查這一步)。
通過上面的分析可以看出,對于JavaScript引擎來說,肯定有詞法分析和語法分析,之后可能還有語義檢查、代碼優化等步驟,等這些編譯步驟完成之后(任何語言都有編譯過程,只是解釋型語言沒有編譯成二進制代碼),才會開始執行代碼。
上面的編譯過程,還是無法更深入的解釋文章開頭部分的“預解析”,我們還得仔細探究下JavaScript代碼的執行過程。
執行過程
周愛民在《JavaScript語言精髓與編程實踐》的第二部分,對此有非常仔細的分析。下面是我的一些領悟:
通過編譯,JavaScript代碼已經翻譯成了語法樹,然后會立刻按照語法樹執行。
進一步的執行過程,需要理解JavaScript的作用域機制,JavaScript采用的是詞法作用域(lexcical scope)。通俗地講,就是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語言的很多現象都能迎刃而解。
小結
至此,對于文章開頭部分的疑問,可以解釋得很清楚了:
step3中所謂的“預解析”,其實是在step2的語法分析階段完成,并存儲在語法樹中。當執行到函數實例時,會將varDelcs和 funcDecls從語法樹中復制到執行環境的scriptObject上。
step4中,未定義變量意味著在scriptObject的變量表中找不到,JS引擎會沿著scriptObject的upvalue往上尋找,如果都沒找到,對于寫操作i = 1; 最后就會等價為 window.i = 1; 給window對象新增了一個屬性。對于讀操作,如果一直追溯到全局執行環境的scriptObject上都找不到,就會產生運行期錯誤。
理解后,霧散花開,天空一片晴朗。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
