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