JavaScript運行機制淺探

來源:互聯網
上載者:User
文章目錄
  • 編譯過程
  • 執行過程
  • 小結

從一個簡單的問題談起:

<script type="text/javascript">    alert(i); // ?    var i = 1;</script>

輸出結果是undefined, 這種現象被稱成“預解析”:JavaScript引擎會優先解析var變數和function定義。在預解析完成後,才會執行代碼。如果一個文檔流中包含多個script程式碼片段(用script標籤分隔的js代碼或引入的js檔案),運行順序是:

step1. 讀入第一個程式碼片段step2. 做文法分析,有錯則報語法錯誤(比如括弧不匹配等),並跳轉到step5step3. 對var變數和function定義做“預解析”(永遠不會報錯的,因為只解析正確的聲明)step4. 執行程式碼片段,有錯則報錯(比如變數未定義)step5. 如果還有下一個程式碼片段,則讀入下一個程式碼片段,重複step2step6. 結束

上面的分析,已經能解釋很多問題了,但老覺得欠缺點什麼。比如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"EQUALSNAME "a"MINUSNAME "b"SEMICOLON

上面只是樣本,更進一步的瞭解請查看 Lexical Analysis.

《JavaScript權威指南》的第2章,講的就是詞法結構(Lexical Structure),ECMA-262 中也有描述。詞法結構是一門語言的基礎,很容易掌握。至於詞法分析的實現那是另一個研究領域,在此不探究。

可以拿自然語言來類比,詞法分析是一對一的硬性翻譯,比如一段英文,逐詞翻譯成中文,得到的是一堆記號流,還很難理解。進一步的翻譯,就需要文法分析了,是一個條件陳述式的文法樹:

構造文法樹的時候,如果發現無法構造,比如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">    var arg = 1;    function foo(arg) {        alert(arg);        var arg = 2;    }    foo(3);</script>

請問alert的輸出是什嗎?

參考資料
  • 周愛民《JavaScript語言精髓與編程實踐》(推薦仔細閱讀第二部分,愛民的書,總是很耐讀)
  • 實現一個指令碼引擎(引擎不是我們想象中的那麼深奧)
  • JavaScript Closures(絕對的好文,推薦)
  • hax: 簡述JavaScript的scope機理(言簡意賅)
後序

這篇小文寫了一周多才寫完。JavaScript語言,有點像大學時學量子力學,剛以為明白了,想想又糊塗了。糊塗過後,仔細研究,一切如霽月般明朗時,突然間一個想法,一個思維陷阱,又會掉進糊塗中。但無論糊塗還是明白,保持學習的心態,總歸是會越發清晰起來的。

本文來自:http://lifesinger.org/blog/2009/01/javascript-run-mechanism/

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.