做者:東北烤冷麪@毛豆前端javascript
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。前端
抽象語法樹在不少領域有普遍的應用,好比瀏覽器,智能編輯器,編譯器等。在JavaScript中,雖然咱們並不會經常與AST直接打交道,但卻也會常常的涉及到它。例如使用UglifyJS來壓縮代碼,bable對代碼進行轉換,ts類型檢查,語法高亮等,實際這背後就是在對JavaScript的抽象語法樹進行操做。java
javascript的抽象語法樹的生成主要依靠的是Javascript Parser(js解析器),整個解析過程分爲兩個階段: node
詞法分析是計算機科學中將字符序列轉換爲單詞(Token)序列的過程,進行詞法分析的程序叫作詞法分析器,也叫掃描器(Scanner)。webpack
//code let age='18' //tokens [ { value: 'let', type:'identifier' }, { type:'whitespace', value:' ' }, { value: 'age', type:'identifier' }, { value: '=', type:'operator' }, { value: '=', type:'operator' }, { value: '18', type:'num' }, ] 複製代碼
語法分析是編譯過程的一個邏輯階段。語法分析的任務是在詞法分析的基礎上將單詞序列組合成語法樹,如「程序」,「語句」,「表達式」等等.語法分析程序判斷源程序在結構上是否正確。源程序的結構由上下文無關文法描述。git
{ "type": "Program", "start": 0, "end": 12, "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "age" }, "init": { "type": "Literal", "value": "18", "raw": "'18'" } } ], "kind": "let" } ], "sourceType": "module" } 複製代碼
常見的Javascript Parser有不少:github
Babel是一個經常使用的工具,它的工做過程通過三個階段,解析(parsing)、轉換(transform)、生成(generate),以下圖所示,在parse階段,babel使用babylon庫將源代碼轉換爲AST,在transform階段,利用各類插件進行代碼轉換,在generator階段,再利用代碼生成工具,將AST轉換成代碼。
web
咱們想在代碼中的console打印出來的內容前面加上它所在的函數名稱,代碼以下:編程
// index.js function compile(code) { // todo } const code = ` function foo(){ console.log('bar') } ` const result = compile(code) console.log(result.code) 複製代碼
首先咱們先安裝bable的全家桶工具:瀏覽器
yarn add @babel/{parser,traverse,types,generator}
複製代碼
而後將其引入文件中:
const generator = require("@babel/generator") const parser = require("@babel/parser") const traverse = require("@babel/traverse") const t = require("@babel/types") function compile(code) { //tode } const code = ` function foo(){ console.log('bar') } ` const result = compile(code) console.log(result.code) 複製代碼
咱們能夠經過AST Explorer查看code代碼的抽象語法樹結構,注意,這裏面咱們的解析工具要選用babylon7,這樣和咱們例子中代碼解析出的結構才匹配
先解析拿到AST,直接生成代碼片斷:
const generator = require("@babel/generator") const parser = require("@babel/parser") const traverse = require("@babel/traverse") const t = require("@babel/types") function compile(code) { // 1. 解析 const ast = parser.parse(code) // 2. 遍歷 // 3. 生成代碼片斷 return generator.default(ast, {}, code) } const code = ` function foo(){ console.log('bar') } ` const result = compile(code) console.log(result.code) 複製代碼
運行一下
node index.js
複製代碼
輸出結果
第二階段
須要使用到訪問者(Visitors),訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。這麼說有些抽象因此讓咱們來看一個例子。
const MyVisitor = { Identifier() { console.log("Called!"); } }; // 你也能夠先建立一個訪問者對象,並在稍後給它添加方法。 let visitor = {}; visitor.MemberExpression = function() {}; visitor.FunctionDeclaration = function() {} 複製代碼
這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中碰見一個 Identifier
的時候會調用 Identifier()
方法。
因此在下面的代碼中 Identifier()
方法會被調用四次(包括 square
在內,總共有四個 Identifier
)。).
function square(n) { return n * n; } path.traverse(MyVisitor); Called! Called! Called! Called! 複製代碼
回到咱們的例子,咱們只須要建立一個訪問者,訪問到CallExpression節點,而後經過判斷,去修改它arguments屬性的參數就能夠完成咱們的任務了
const generator = require("@babel/generator") const parser = require("@babel/parser") const traverse = require("@babel/traverse") const t = require("@babel/types") function compile(code) { // 1. 解析 const ast = parser.parse(code) // 2. 遍歷 //visitor能夠對特定節點進行處理 const visitor = { //定義須要轉換的節點CallExpression CallExpression(path) { //獲取當前的節點 const { callee } = path.node; //判斷 if ( t.isMemberExpression(callee) && callee.object.name === 'console' && callee.property.name === 'log' ) { // 獲取上層FunctionDeclaration路徑 const funcPath = path.findParent(p => { return p.isFunctionDeclaration(); }) // 將上層函數名添加到參數前 path.node.arguments.unshift( t.stringLiteral(`function name ${funcPath.node.id.name}:`) ) } } } traverse.default(ast, visitor) // 3. 生成代碼片斷 return generator.default(ast, {}, code) } const code = ` function foo(){ console.log('bar') } ` const result = compile(code) console.log(result.code) 複製代碼
咱們再來打印下
這樣咱們就完成了整個任務,固然這只是一個很簡單的例子,在實際開發中,咱們還須要進行更復雜的判斷才能保證咱們的功能完善。