你怎么可以不了解 AST 呢?
前言
我們編寫業務代碼的時候,可能很少人會使用到AST,以至于大多數同學都不大了解AST。有的同學曾經學過,但是不去實踐的話,過段時間又忘的差不多了。看到這里,你會發現說的就是你。聽說貴圈現在寫文章都要編故事,時不時還要整點表情包。這是真的嗎?作為公司最頭鐵的前端,我就不放。
本文將通過以下幾個方面對AST進行學習
1.基礎知識
- AST是什么
- AST有什么用
- AST如何生成
2.實戰小例子
- 去掉debugger
- 修改函數中執行的console.log參數
3.總結
基礎知識
AST是什么先貼下官方的解釋
- 在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。
為了方便大家理解抽象語法樹,來看看具體的例子。
- var tree = 'this is tree'
js 源代碼將會被轉化成下面的抽象語法樹
- {
- "type": "Program",
- "start": 0,
- "end": 25,
- "body": [
- {
- "type": "VariableDeclaration",
- "start": 0,
- "end": 25,
- "declarations": [
- {
- "type": "VariableDeclarator",
- "start": 4,
- "end": 25,
- "id": {
- "type": "Identifier",
- "start": 4,
- "end": 8,
- "name": "tree"
- },
- "init": {
- "type": "Literal",
- "start": 11,
- "end": 25,
- "value": "this is tree",
- "raw": "'this is tree'"
- }
- }
- ],
- "kind": "var"
- }
- ],
- "sourceType": "module"
- }
可以看到一條語句由若干個詞法單元組成。這個詞法單元就像 26 個字母。創造出個十幾萬的單詞,通過不同單詞的組合又能寫出不同內容的文章。
至于有哪些詞法單元可點擊查看AST 對象文檔 或者 參考掘金大佬的文章高級前端基礎-JavaScript 抽象語法樹 AST里面列舉了語法樹節點與解釋。
推薦一個工具 在線 ast 轉換器。可以在這個網站上,親自嘗試下轉換。點擊語句中的詞,右邊的抽象語法樹節點便會被選中,如下圖:
AST 有什么用
- IDE 的錯誤提示、代碼格式化、代碼高亮、代碼自動補全等
- JSLint、JSHint 對代碼錯誤或風格的檢查等
- webpack、rollup 進行代碼打包等
- CoffeeScript、TypeScript、JSX 等轉化為原生 Javascript.
- vue 模板編譯、react 模板編譯
AST 如何生成
看到這里,你應該已經知道抽象語法樹大致長什么樣了。那么AST又是如何生成的呢?
AST 整個解析過程分為兩個步驟
- 詞法分析 (Lexical Analysis):掃描輸入的源代碼字符串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等。詞法單元之間都是獨立的,也即在該階段我們并不關心每一行代碼是通過什么方式組合在一起的。
- 語法分析 (Syntax Analysis):建立分析語法單元之間的關系
還是以上面var tree = 'this is tree'為例
正規理解
詞法分析
先經過詞法分析,掃描輸入的源代碼字符串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等
語法分析
語法分析階段就會將上一階段生成的 tokens 列表轉換為如下圖所示的 AST(我把start、end字段去掉了不用在意)
非正規理解
鄭重聲明:我周某人語文很少及格,大致意思能理解就好。
例子:"它是豬。"
詞法分析
先經過詞法分析,掃描輸入的源代碼字符串,生成一系列的詞法單元 (tokens)。這些詞法單元包括數字,標點符號,運算符等
語法分析
語法分析階段就會將上一階段生成的 tokens 列表轉換為如下圖所示的 AST
JsParser
JavaScript Parser,把 js 源碼轉化為抽象語法樹的解析器。
- acorn
- esprima
- traceur
- @babel/parser
實戰小例子
例子 1:去 debugger
源代碼:
- function fn() {
- console.log('debugger')
- debugger;
- }
根據前面學過的知識點,我們先腦海中意淫下如何去掉這個debugger
- 先將源代碼轉化成AST
- 遍歷**AST**上的節點,找到**debugger**節點,并刪除
- 將轉換過的AST再生成JS代碼
將源代碼拷貝到 在線 ast 轉換器 中,并點擊左邊區域的debugger,可以看到左邊的debugger節點就被選中了。所以只要把圖中選中的debugger抽象語法樹節點刪除就行了。
這個例子比較簡單,直接上代碼。
這個例子我使用@babel/parser、@babel/traverse、@babel/generator,它們的作用分別是解析、轉換、生成。
- const parser = require('@babel/parser');
- const traverse = require("@babel/traverse");
- const generator = require("@babel/generator");
- // 源代碼
- const code = `
- function fn() {
- console.log('debugger')
- debugger;
- }
- `;
- // 1. 源代碼解析成 ast
- const ast = parser.parse(code);
- // 2. 轉換
- const visitor = {
- // traverse 會遍歷樹節點,只要節點的 type 在 visitor 對象中出現,變化調用該方法
- DebuggerStatement(path) {
- // 刪除該抽象語法樹節點
- path.remove();
- }
- }
- traverse.default(ast, visitor);
- // 3. 生成
- const result = generator.default(ast, {}, code);
- console.log(result.code)
- // 4. 日志輸出
- // function fn() {
- // console.log('debugger');
- // }
babel核心邏輯處理都在visitor里。traverse會遍歷樹節點,只要節點的type在visitor對象中出現,便會調用該type對應的方法,在方法中調用path.remove()將當前節點刪除。demo中使用到的path的一些api可以參考babel-handbook。
例子 2:修改函數中執行的 console.log 參數
我們有時候在函數里打了日志,但是又想在控制臺直觀的看出是哪個函數中打的日志,這個時候就可以使用AST,去解析、轉換、生成最后想要的代碼。
源代碼:
- function funA() {
- console.log(1)
- }
- // 轉換成
- function funA() {
- console.log('from function funA:', 1)
- }
在編碼之前,我們先理清思路,再下手也不遲。這個時候就需要借助 在線 ast 轉換器來分析了。
通過工具發現想要實現這個案例只需要往arguments前面插入段節點就可以了。
這里也像例子 1 一樣先梳理下思路
- 使用 @babel/parser 將源代碼解析成 ast
- 監聽 @babel/traverse 遍歷到 CallExpression
- 觸發后,判斷如果執行的方法是 console.log 時,往 arguments unshift一個 StringLiteral
- 將轉換后的 ast 生成代碼
將 js 代碼解析成 ast 與 將 ast 生成 js 代碼與去 debugger 例子一致,這里將不再描述。
首先監聽CallExpression遍歷
- const visitor = {
- CallExpression(path) {
- // console.log(path)
- }
- }
觀察 在線 ast 轉換器 解析后的樹,我們只要判斷path 的 callee中存在對象 console 及屬性 property 。就可以往當前的 path 的 arguments unshift 一個 StringLiteral
這里的 types 對象是使用了一個新包 @babel/types , 用來判斷類型。
上面用到的isMemberExpression,isIdentifier,getFunctionParent,stringLiteral都可以在babel-handbook文檔中找到,本文就不解釋了。
- const visitor = {
- // 當遍歷到 CallExpression 時候觸發
- CallExpression(path) {
- const callee = path.node.callee;
- // 判斷當前當前執行的函數是否是組合表達式
- if (types.isMemberExpression(callee)) {
- const { object, property } = callee;
- if (types.isIdentifier(object, { name: 'console' }) && types.isIdentifier(property, { name: 'log' })) {
- // 查找最接近的父函數或程序
- const parent = path.getFunctionParent();
- const parentFunName = parent.node.id.name;
- path.node.arguments.unshift(types.stringLiteral(`from function ${parentFunName}`))
- }
- }
- }
- }
總結
就像前言所說的,我們的日常工作中很少會去使用AST,以至于大多數同學都不大了解AST。但了解了 AST 可以幫助我們更好地理解開發工具、編譯器的原理,并產出提高代碼效率的工具。還記得我在之前的前端小組遇到一個問題,我們項目是SSR項目,在服務端執行的生命周期不允許出現客戶端才能執行的代碼。但是小組成員有時候無意的寫了,導致服務端渲染降級。在學習AST之前,我為了解決這個問題,寫了個loader通過正則去匹配校驗,當時可真是逼死我了,正則需要去適配各種場景。后面我學習了AST了之后,編寫了個eslint插件實現了客戶端代碼校驗。
參考
- babel-handbook(https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md)
- 深入 Babel,這一篇就夠了(https://juejin.im/post/6844903746804137991)
- 高級前端基礎-JavaScript 抽象語法樹 AST(https://juejin.cn/post/6844903798347939853#heading-12)