js編譯原理(你不知道的javascript)


雖然通常將js歸類為“動態”或“解釋執行”語言,但其實也可把它看成是一門編譯語言。只不過這個所謂的編譯與傳統的編譯語言不同,它不是提前編譯的,編譯結果也不能在分布式系統中進行移植。對於js來說,它的編譯過程不是發生在構建之前的,大部分情況下編譯發生在代碼執行前的幾微秒甚至更短的時間內。甚至是代碼執行中

為甚么懷疑js不是解釋型語言?

如果是解釋型語言,變量聲明提升為什么會發生?
JIT(及時編譯)做代碼優化(同時生成編譯版本);解釋型語言無法做到這些(解釋型語言幾乎在執行后一瞬間就開始,幾乎沒有任何代碼優化)

編譯型語言 vs 解釋型語言

編譯型語言是代碼在運行前編譯器將人類可以理解的語言(編程語言)轉換成機器可以理解的語言
解釋型語言也是人類可以理解的語言(編程語言),也需要轉換成機器可以理解的語言才能執行,但是是在運行時轉換的。所以執行前需要解釋器安裝在環境中;但是編譯型語言編寫的應用在編譯后能直接運行。

許多人認為解釋型語言意味着當遇到程序中行號為xyz時直接將其傳給CPU就能運行;但是事實不是這樣。所有的編程語言都是為人類創建的。他們是人類能夠理解的。你必須將編程語言轉換為機器語言。編譯器獲取整個代碼,轉換它,做合適的優化並且創建一個可以運行的輸出文件。編譯器根據上下文來轉換語句。

聲明提升

js的聲明提升:在函數作用域內的任何變量的聲明都會被提升到頂部並且是undeinfed值。
所以JavaScript引擎解釋同樣的腳本文件兩次?做完所有的聲明提升然后執行代碼?還是先編譯整個代碼然后運行它?這兩種情況都不對。

下面是js處理聲明的過程:
一旦V8引擎進入一個執行具體代碼的執行上下文(函數),它就對代碼進行詞法分析或者分詞。這意味着代碼將被分割成像foo = 10這樣的原子標記。
在對當前的整個作用域分析完成后,引擎將解析成一個AST(抽象語法書)的翻譯版本。
引擎每一次遇到聲明,它就把聲明傳到作用域來創建一個綁定。對每一次聲明它都會為變量分配內存。只是分配內存而不是把代碼修改成聲明提升。正如你所知道的,在JS中分配內存意味着將默認值設為undefined。
在這之后,引擎每一次遇到賦值或者取值,它都會通過作用域查找綁定。如果在當前作用域中沒有查找到就接着向上級作用域查找直到找到為止。
接着引擎生成CPU可以執行的機器碼。
最后, 代碼執行完畢。

所以變量提升不過是執行上下文的游戲,而不是網站描述的代碼修改。在執行任何語句之前,解釋器就已經從運行上下文創建的作用域中找到變量的值了。

進一步補充,js對程序var a=2的處理過程

基本術語介紹
1.分詞/詞法分析
這些代碼塊被稱為詞法單元(token)。例如,var a = 2;。這段程序通常會被分解成為下面這些詞法單元:var、a、=、2 、;

2.解析/語法分析
這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)

3.代碼生成
將AST轉換為可執行代碼的過程稱被稱為代碼生成 。 
參與該過程角色
引擎
從頭到尾負責整個JavaScript程序的編譯及執行過程。

編譯器
引擎的好朋友之一,負責語法分析及代碼生成等臟活累活。

作用域
引擎的另一位好朋友,負責收集並維護由所有聲明的標識符(變量)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限
工作流程
編譯器首先將這段程序分解成詞法單元,然后將詞法單元解析成一個樹結構。
當編譯器進行代碼生成時:

1.遇到var a,編譯器會詢問作用域是否已經有一個該名稱的變量存在同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續進行編譯;否則它會要求作用域在當前作用域的集合中聲明一個新的變量,並命名為a

2.接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a=2這個賦值操作。引擎運行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作a的變量。如果是,引擎就會使用這個變量;如果否,引擎會繼續查找該變量。如果引擎最終找到了a變量,就會將2賦值給它。否則就會拋出異常

總結:
變量的賦值操作會執行兩個動作,首先編譯器會在當前作用域中聲明一個變量(如果之前沒聲明過),然后在運行時引擎會在作用域中查找該變量,如果能夠找到就會對它進行賦值。

解釋JavaScript中的即時編譯(JIT)

IT或者說及時編譯編譯器不是JavaScript所特有的。像Java這樣的其他語言也有一些在執行前編譯代碼的機制。

現代JavaScript引擎同樣有JIT。是的,它們有編譯器。讓我來為你解釋一下為什么它們需要JIT以及JIT如何在JavaScript的執行中起作用。

編譯型和解釋型語言最重要的區別是編譯語言話很長的時間來准備執行。因為它需要對整個代碼進行詞法分析、做一些極致的優化等工作。另一方面解釋型語言幾乎在執行后一瞬間就開始,但是沒有任何代碼優化。所以沒一條語句都是分開轉換的,考慮下面這一段代碼。

for(i=0; i<1000; i++){
    sum += i;
}
在編譯型語言中sum += i部分在循環運行時已經編譯成了機器碼,機器碼將直接運行一千次。

但是在解釋型語言中,他會在執行時將sum += i解釋一千次。所以因為對相同的代碼進行一千次轉換會造成非常大的性能損耗。

這就是Google和Mozilla的開發人員將JIT加入JavaScript的原因。
編譯
在JavaScript中如果一段代碼運行超過一次,那么就稱為warm。如果一個函數開始變得更加warm(譯者注:運行更多次),JIT將把這段代碼送到編譯器中編譯並且保存一個編譯后的版本。下一次同樣代碼執行的時候,引擎會跳過翻譯過程直接使用編譯后的版本。

這將優化性能。在真正的編譯器中,因為編譯器能訪問整個代碼所以能做更多的事。
優化
如果一段warm代碼變得hot或者hotter(譯者注:指運行更多次以及比更多還要多的次數)JIT會嘗試更多的優化並且保存優化后的版本。在編譯器進行優化的過程中會做一些關於變量類型和環境中值的假設;但是如果假設不成立就將這個優化的版本回退,如果假設成立的話,這將讓代碼性能更高。
總結
JavaScript代碼需要在機器(node或者瀏覽器)上安裝一個工具(JS引擎)才能執行。這是解釋型語言需要的。編譯型語言產品能夠自由地直接運行。
聲明提升等不是代碼修改。在這個過程中沒有生成中間代碼。這只是JS解釋器處理事情的方式。
JIT是唯一一點我們可以對JavaScript是否是一個解釋型語言提出疑問的理由。但是JIT不是純粹的編譯器,它在執行前進行編譯。而且JIT知識Mozilla和Google的開發人員為了在他們的瀏覽器產品中提升性能才引入的。JavaScript或TC39從來沒有要求這樣做

參考鏈接:
https://segmentfault.com/a/1190000013126460
參考書籍:
《你不知道的js》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM