- 前端跨界開發指南:JavaScript工具庫原理解析與實戰
- 史文強
- 2859字
- 2022-08-12 16:06:21
3.5 靜態類型檢查工具的實現原理
代碼檢查是一種靜態分析的方法,用于尋找有問題的模式或代碼。代碼檢查工具極大地提高了開發者代碼評審的效率,并有效減少了工作量,在享受它帶來的便利性的同時,我們也應該思考它是如何實現檢查功能的。如果你使用過Node.js,就不難想到通過fs模塊也可以將整個文件以字符串的形式讀取到我們的程序中,那么拿到字符串文本之后,又該如何分析呢?事實上,這個問題會引出前端領域一個非常重要,對于初級開發者而言卻非常陌生的概念——編譯,它不僅僅是ESLint的基礎,包括大名鼎鼎的Babel、webpack以及你每天都在用的Vue、Angular、React框架都離不開這個重要的知識點。
3.5.1 編譯語言和解釋語言
編程語言可分為編譯語言和解釋語言。無論哪種語言,在最終執行前都會被翻譯成機器能夠識別的機器碼,但編譯語言和解釋語言被翻譯的時機是不同的。舉例來說,編譯語言就像是先做好一桌菜再開吃,而解釋語言則更像是吃火鍋,邊煮邊吃,所以也就不難理解為什么解釋語言在運行時效率更低了。
編譯語言在編寫完成后并不能直接使用,而是需要先將其編譯為計算機可以識別的機器碼,這樣計算機才能夠運行高級語言所實現的功能。由于其提前完成了翻譯工作,所以執行的時候速度更快,但缺點也是顯而易見的,因為不同的平臺能夠識別的機器碼并不相同,所需要的編譯器也不一樣。所以,高級語言會使用一種被稱為“字節碼”的技術將高級語言所編寫的程序編譯為虛擬機能夠識別的中間狀態的二進制編碼,而將跨平臺的兼容性放在虛擬機中來實現,從而兼顧編程語言的跨平臺特性和運行效率。而解釋語言則會在執行虛擬機中一邊翻譯一邊執行,也就是我們常說的即時編譯(Just-In-Time Compilation),不難理解其優缺點與編譯語言正好是對立的。JavaScript就是一種解釋型語言。
3.5.2 編譯流程
傳統編譯型語言的編譯過程大致需要經過如下幾個典型階段。
1. 分詞分析(Lexical Analysis)階段
編譯器將字符串序列分割成若干個具有一定意義的字符串單元,也稱為詞法單元(token),分詞所依賴的策略會依據不同語言的特點來制定,其結果一般會以數組的形式標記出每個詞法單元的類型和原始字符串,比如,某個編譯器可能會使用identifier(標識符)、number(數值)、operator(操作符)、punctuation(標點符號)等來標識詞法單元的類型。
2. 語法分析(Syntactic Analysis)階段
語法分析是在詞法分析的基礎上進行的,它會嘗試將詞法單元組合成為符合一定語法規范的語句,如果詞法單元的序列無法拼接成合法的語法,就說明源程序出現了語法錯誤。比如在JavaScript語法中,一個標識符加上一個等號,再加上一個數值或者另一個標識符,就可以組成一個賦值語句。語法分析轉換后的形式一般稱為“抽象語法樹”(Abstract Syntax Tree,AST)。
3. 遍歷分析(Traversal Analysis)階段
遍歷分析是在抽象語法樹的基礎上進行的,其依據一個自定義的策略集合(可能是語法轉換策略,也可能是針對抽象語法樹中某些特定類型節點的檢查或優化策略)對相應的部分進行操作,我們可以對抽象語法樹中的節點進行增刪改查操作(如果你已經掌握了一些基本的數據結構和算法知識,就不難意識到,抽象語法樹的本質就是樹,所有對于樹型結構的抽象理論和運算都可以用于抽象語法樹)。
4. 代碼生成(Code Generation)階段
在這個階段中,編譯器會將抽象語法樹轉換為可執行代碼,或者一組機器指令,這個過程與使用的平臺密切相關,當你在編譯參數中指定不同的適用系統時,最終生成的結果通常也不相同。這就好像是我們熟悉的React,在架構設計中就引入了獨立的渲染層,引入不同的渲染器模塊可以將同樣的應用層代碼渲染到不同的平臺中。同理,當我們使用不同的代碼生成器時,理論上也可以將同一個抽象語法樹結構轉換為其他語言的代碼。
3.5.3 編譯簡單的JavaScript程序
本節將通過一段簡單的示例代碼來展示編譯過程,它能夠幫助我們更直觀地理解編譯器的工作:
/*源代碼*/ var a = 1; /** * 1. 詞法分析: * type-詞法類型, * value-原始字符串, * start-詞法單元開始位置, * end-詞法單元結束位置 */ [ {type:'Keyword', value:'var', start:0, end:3}, {type:'Identifier', value:'a', start:4, end:5}, {type:'Punctuator', value:'=', start:6, end:7}, {type:'Numeric', value:'1', start:8, end:9}, {type:'Punctuator', value:';', start:9, end:10} ] /** * 2. 語法分析: * type:'Program'程序段, * type:'VariableDeclaration'-變量聲明語句, * type:'VariableDeclarator'-變量聲明表達式, * type:'Identifier'-標識符, * type:'Literal'-字面量 */ { "type": "Program", "start": 0, "end": 10, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 10, "declarations": [ { "type": "VariableDeclarator", "start": 4, "end": 9, "id": { "type": "Identifier", "start": 4, "end": 5, "name": "a" }, "init": { "type": "Literal", "start": 8, "end": 9, "value": 1, "raw": "1" } } ], "kind": "var" } ], "sourceType": "module" }
看到抽象語法樹的“廬山真面目”后,代碼靜態類型檢查的實現思路就變得很清晰了,下面試著用偽代碼實現一些常見的靜態類型檢查項目:
//1. 聲明變量時必須為其賦初始值 if (node.type === 'VariableDeclarator' && node.init === null){ console.log('變量必須初始化'); } //2. 每一個變量聲明語句只能聲明一個變量 if (node.type === 'VariableDeclaration' && node.declarations.length > 1){ console.log('存在聲明語句聲明了多個變量的情況'); } //3. 標識符指向對象類型時,變量需要用關鍵字const進行聲明 if (node.type === 'VariableDeclaration'){ node.declarations.map(declarator=>{ if (declarator.init && declarator.init.type === 'ObjectExpression' && node.kind !== 'const'){ console.log('標識符指向對象時需要使用const進行聲明'); } }); }
可能有讀者已經察覺到,對于編譯過程來說,代碼本身就是數據。上面的過程只演示了抽象語法樹分析工作的冰山一角,我們還需要學習一些樹結構遍歷的基本算法,才能對抽象語法樹的遍歷分析有更好的理解。至此,編譯工作還差最后一步,那就是生產代碼。這個環節比較神奇,抽象語法樹實際上是一個關鍵信息的聚合結構,換句話說,它與語言并不是強耦合的。對于抽象語法樹里的每一種語法類型,編譯器都會有對應的代碼字符串生成策略(當然,有的策略是依賴于配置參數的),假如我們的編譯目標是“中文”或“機器碼”,那么最終產生的代碼可能會是下面這個樣子:
//假設從'VariableDeclarator'節點開始分析 /** * 編譯成中文 * 生成策略:`變量聲明 ${node.id.name} 初始化賦值為 ${node.init.value} ;` * 輸出結果: 變量聲明 a 初始化賦值 為 1 ; */ /** * 編譯成機器碼 * 生成策略:(略) * 輸出結果: 01001001 00101010...... */
生成了代碼之后,只需要將它寫到編譯結果目錄下的指定文件中即可,這個步驟通常稱為“emit”。至此,我們完成了一段簡易的JavaScript代碼的編譯工作。你可以在astexplorer.net網站上方便地將JavaScript代碼轉換為抽象語法樹,從而了解其他語法的轉換結果。
拓展知識
掌握編譯原理是初級前端開發者進階非常重要的知識儲備,前端領域的所有重要技術幾乎都與它有關,但其學習難度也非常大。如果你對相關的內容感興趣,可以從下面幾個項目著手學習。
(1)The-Super-Tiny-Compiler[1]
這個項目在GitHub上有2.1萬顆星星,它實現了一個極簡卻“五臟俱全”的編譯器,代碼中包含非常豐富的注釋和知識講解,建議反復觀摩源代碼來學習,直到能夠自行實現或默寫。
(2)Espree
它是著名的JavaScript代碼編譯工具,也是ESLint使用的解析器,可以調用它提供的方法來查看一些典型的代碼分詞結果和抽象語法樹。
(3)Acorn
大名鼎鼎的Babel在版本升級中基于Acorn解析器定制了自己的@babel/parser,如果你對抽象語法樹的解析效率感興趣,可以自行研究、對比不同的解析器。
(4)ASTexplorer
在線AST轉換工具,可以實時地將JavaScript代碼轉換成為抽象語法樹,當開發者編寫ESLint插件或是Babel插件等任何依賴于AST轉換的工具時,經常會使用它來查看目標節點的類型。
(5)“編譯原理”公開課
如果時間和精力都允許,可以嘗試觀看斯坦福大學的“編譯原理”公開課,以便系統地學習編譯相關知識,當然大多數前端工程師可能并沒有機會使用到這么深入的知識,斯坦福大學的官方線上學習平臺[2]或B站[3]上都可以找到相關視頻(B站有中文字幕)。
[1]源代碼倉庫地址:https://github.com/jamiebuilds/the-super-tiny-compiler。
[2]online.stanford.edu/lagunita-learning-platform斯坦福大學官方在線學習平臺,其中包含了大量計算機相關課程。
[3]bilibili.com,知名視頻網站。
- 零基礎搭建量化投資系統:以Python為工具
- Learning Docker
- Python深度學習
- JavaFX Essentials
- 匯編語言程序設計(第2版)
- STM32F0實戰:基于HAL庫開發
- 用Python實現深度學習框架
- Serverless架構
- 移動增值應用開發技術導論
- Swift語言實戰晉級
- Apache Solr for Indexing Data
- Flutter之旅
- Building Web and Mobile ArcGIS Server Applications with JavaScript(Second Edition)
- Web前端開發技術實踐指導教程
- Mastering Magento Theme Design