<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)eb技術(shù)】751- 一文助你搞懂 AST

          共 16523字,需瀏覽 34分鐘

           ·

          2020-10-20 02:09


          好朋友在團隊分享的文章
          作者:fecym

          原文地址:https://chengyuming.cn/views/webpack/AST.html

          什么是 AST

          抽象語法樹(Abstract Syntax Tree)簡稱 AST,是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式。webpackeslint 等很多工具庫的核心都是通過抽象語法書這個概念來實現(xiàn)對代碼的檢查、分析等操作。今天我為大家分享一下 JavaScript 這類解釋型語言的抽象語法樹的概念

          我們常用的瀏覽器就是通過將 js 代碼轉(zhuǎn)化為抽象語法樹來進行下一步的分析等其他操作。所以將 js 轉(zhuǎn)化為抽象語法樹更利于程序的分析。

          如上圖中變量聲明語句,轉(zhuǎn)換為 AST 之后就是右圖中顯示的樣式

          左圖中對應的:

          • var 是一個關(guān)鍵字
          • AST 是一個定義者
          • = 是 Equal 等號的叫法有很多形式,在后面我們還會看到
          • is tree 是一個字符串
          • ; 就是 Semicoion

          首先一段代碼轉(zhuǎn)換成的抽象語法樹是一個對象,該對象會有一個頂級的 type 屬性 Program;第二個屬性是 body 是一個數(shù)組。

          body 數(shù)組中存放的每一項都是一個對象,里面包含了所有的對于該語句的描述信息

          type:?????????描述該語句的類型??-->?變量聲明的語句
          kind:?????????變量聲明的關(guān)鍵字??-->?var
          declaration:??聲明內(nèi)容的數(shù)組,里面每一項也是一個對象
          ????????????type:?描述該語句的類型
          ????????????id:???描述變量名稱的對象
          ????????????????type:?定義
          ????????????????name:?變量的名字
          ????????????init:?初始化變量值的對象
          ????????????????type:???類型
          ????????????????value:??值?"is?tree"?不帶引號
          ????????????????row:????"\"is?tree"\"?帶引號

          詞法分析和語法分析

          JavaScript 是解釋型語言,一般通過 詞法分析 -> 語法分析 -> 語法樹,就可以開始解釋執(zhí)行了

          詞法分析:也叫掃描,是將字符流轉(zhuǎn)換為記號流(tokens),它會讀取我們的代碼然后按照一定的規(guī)則合成一個個的標識

          比如說:var a = 2 ,這段代碼通常會被分解成 var、a、=、2

          ;[
          ??{?type:?'Keyword',?value:?'var'?},
          ??{?type:?'Identifier',?value:?'a'?},
          ??{?type:?'Punctuator',?value:?'='?},
          ??{?type:?'Numeric',?value:?'2'?},
          ]

          當詞法分析源代碼的時候,它會一個一個字符的讀取代碼,所以很形象地稱之為掃描 - scans。當它遇到空格、操作符,或者特殊符號的時候,它會認為一個話已經(jīng)完成了。

          語法分析:也稱解析器,將詞法分析出來的數(shù)組轉(zhuǎn)換成樹的形式,同時驗證語法。語法如果有錯的話,拋出語法錯誤。

          {  ...  "type": "VariableDeclarator",  "id": {    "type": "Identifier",    "name": "a"  },  ...}

          語法分析成 AST ,我們可以在這里在線看到效果 http://esprima.org

          AST 能做什么

          • 語法檢查、代碼風格檢查、格式化代碼、語法高亮、錯誤提示、自動補全等
          • 代碼混淆壓縮
          • 優(yōu)化變更代碼,改變代碼結(jié)構(gòu)等

          比如說,有個函數(shù) function a() {} 我想把它變成 function b() {}

          比如說,在 webpack 中代碼編譯完成后 require('a') --> __webapck__require__("*/**/a.js")

          下面來介紹一套工具,可以把代碼轉(zhuǎn)成語法樹然后改變節(jié)點以及重新生成代碼

          AST 解析流程

          準備工具:

          • esprima:code => ast 代碼轉(zhuǎn) ast
          • estraverse: traverse ast 轉(zhuǎn)換樹
          • escodegen: ast => code

          在推薦一個常用的 AST 在線轉(zhuǎn)換網(wǎng)站:https://astexplorer.net/

          比如說一段代碼 function getUser() {},我們把函數(shù)名字更改為 hello,看代碼流程

          看以下代碼,簡單說明 AST 遍歷流程

          const?esprima?=?require('esprima')
          const?estraverse?=?require('estraverse')
          const?code?=?`function?getUser()?{}`
          //?生成?AST
          const?ast?=?esprima.parseScript(code)
          //?轉(zhuǎn)換?AST,只會遍歷?type?屬性
          //?traverse?方法中有進入和離開兩個鉤子函數(shù)
          estraverse.traverse(ast,?{
          ??enter(node)?{
          ????console.log('enter?->?node.type',?node.type)
          ??},
          ??leave(node)?{
          ????console.log('leave?->?node.type',?node.type)
          ??},
          })

          輸出結(jié)果如下:

          由此可以得到 AST 遍歷的流程是深度優(yōu)先,遍歷過程如下:

          修改函數(shù)名字

          此時我們發(fā)現(xiàn)函數(shù)的名字在 typeIdentifier 的時候就是該函數(shù)的名字,我們就可以直接修改它便可實現(xiàn)一個更改函數(shù)名字的 AST 工具


          //?轉(zhuǎn)換樹
          estraverse.traverse(ast,?{
          ??//?進入離開修改都是可以的
          ??enter(node)?{
          ????console.log('enter?->?node.type',?node.type)
          ????if?(node.type?===?'Identifier')?{
          ??????node.name?=?'hello'
          ????}
          ??},
          ??leave(node)?{
          ????console.log('leave?->?node.type',?node.type)
          ??},
          })
          //?生成新的代碼
          const?result?=?escodegen.generate(ast)
          console.log(result)
          //?function?hello()?{}

          babel 工作原理

          提到 AST 我們肯定會想到 babel,自從 Es6 開始大規(guī)模使用以來,babel 就出現(xiàn)了,它主要解決了就是一些瀏覽器不兼容 Es6 新特性的問題,其實就把 Es6 代碼轉(zhuǎn)換為 Es5 的代碼,兼容所有瀏覽器,babel 轉(zhuǎn)換代碼其實就是用了 AST,babel 與 AST 就有著很一種特別的關(guān)系。

          那么我們就在 babel 的中來使用 AST,看看 babel 是如何編譯代碼的(不講源碼啊)

          需要用到兩個工具包 @babel/core@babel/preset-env

          當我們配置 babel 的時候,不管是在 .babelrc 或者 babel.config.js 文件里面配置的都有 presetsplugins 兩個配置項(還有其他配置項,這里不做介紹)

          插件和預設(shè)的區(qū)別

          //?.babelrc
          {
          ??"presets":?["@babel/preset-env"],
          ??"plugins":?[]
          }

          當我們配置了 presets 中有 @babel/preset-env,那么 @babel/core 就會去找 preset-env 預設(shè)的插件包,它是一套

          babel 核心包并不會去轉(zhuǎn)換代碼,核心包只提供一些核心 API,真正的代碼轉(zhuǎn)換工作由插件或者預設(shè)來完成,比如要轉(zhuǎn)換箭頭函數(shù),會用到這個 plugin,@babel/plugin-transform-arrow-functions,當需要轉(zhuǎn)換的要求增加時,我們不可能去一一配置相應的 plugin,這個時候就可以用到預設(shè)了,也就是 presets。presets 是 plugins 的集合,一個 presets 內(nèi)部包含了很多 plugin。

          babel 插件的使用

          現(xiàn)在我們有一個箭頭函數(shù),要想把它轉(zhuǎn)成普通函數(shù),我們就可以直接這么寫:

          const?babel?=?require('@babel/core')
          const?code?=?`const?fn?=?(a,?b)?=>?a?+?b`
          //?babel?有?transform?方法會幫我們自動遍歷,使用相應的預設(shè)或者插件轉(zhuǎn)換相應的代碼
          const?r?=?babel.transform(code,?{
          ??presets:?['@babel/preset-env'],
          })
          console.log(r.code)
          //?打印結(jié)果如下
          //?"use?strict";
          //?var?fn?=?function?fn()?{?return?a?+?b;?};

          此時我們可以看到最終代碼會被轉(zhuǎn)成普通函數(shù),但是我們,只需要箭頭函數(shù)轉(zhuǎn)通函數(shù)的功能,不需要用這么大一套包,只需要一個箭頭函數(shù)轉(zhuǎn)普通函數(shù)的包,我們其實是可以在 node_modules 下面找到有個叫做 plugin-transform-arrow-functions 的插件,這個插件是專門用來處理 箭頭函數(shù)的,我們就可以這么寫:

          const?r?=?babel.transform(code,?{
          ??plugins:?['@babel/plugin-transform-arrow-functions'],
          })
          console.log(r.code)
          //?打印結(jié)果如下
          //?const?fn?=?function?()?{?return?a?+?b;?};

          我們可以從打印結(jié)果發(fā)現(xiàn)此時并沒有轉(zhuǎn)換我們變量的聲明方式還是 const 聲明,只是轉(zhuǎn)換了箭頭函數(shù)

          編寫自己的插件

          此時,我們就可以自己來寫一些插件,來實現(xiàn)代碼的轉(zhuǎn)換,中間處理代碼的過程就是使用前面提到的 AST 的處理邏輯

          現(xiàn)在我們來個實戰(zhàn)把 const fn = (a, b) => a + b 轉(zhuǎn)換為 const fn = function(a, b) { return a + b }

          分析 AST 結(jié)構(gòu)

          首先我們在在線分析 AST 的網(wǎng)站上分析 const fn = (a, b) => a + bconst fn = function(a, b) { return a + b }看兩者語法樹的區(qū)別


          根據(jù)我們分析可得:

          1. 變成普通函數(shù)之后他就不叫箭頭函數(shù)了 ArrowFunctionExpression,而是函數(shù)表達式了 FunctionExpression
          2. 所以首先我們要把 箭頭函數(shù)表達式(ArrowFunctionExpression) 轉(zhuǎn)換為 函數(shù)表達式(FunctionExpression)
          3. 要把 二進制表達式(BinaryExpression) 放到一個 代碼塊中(BlockStatement)
          4. 其實我們要做就是把一棵樹變成另外一顆樹,說白了其實就是拼成另一顆樹的結(jié)構(gòu),然后生成新的代碼,就可以完成代碼的轉(zhuǎn)換

          訪問者模式

          在 babel 中,我們開發(fā) plugins 的時候要用到訪問者模式,就是說在訪問到某一個路徑的時候進行匹配,然后在對這個節(jié)點進行修改,比如說上面的當我們訪問到 ArrowFunctionExpression 的時候,對 ArrowFunctionExpression 進行修改,變成普通函數(shù)

          那么我們就可以這么寫:

          const?babel?=?require('@babel/core')
          const?code?=?`const?fn?=?(a,?b)?=>?a?+?b`?//?轉(zhuǎn)換后?const?fn?=?function(a,?b)?{?return?a?+?b?}
          const?arrowFnPlugin?=?{
          ??//?訪問者模式
          ??visitor:?{
          ????//?當訪問到某個路徑的時候進行匹配
          ????ArrowFunctionExpression(path)?{
          ??????//?拿到節(jié)點
          ??????const?node?=?path.node
          ??????console.log('ArrowFunctionExpression?->?node',?node)
          ????},
          ??},
          }

          const?r?=?babel.transform(code,?{
          ??plugins:?[arrowFnPlugin],
          })

          console.log(r)

          修改 AST 結(jié)構(gòu)

          此時我們拿到的結(jié)果是這樣的節(jié)點結(jié)果是 這樣的,其實就是 ArrowFunctionExpression 的 AST,此時我們要做的是把 ArrowFunctionExpression 的結(jié)構(gòu)替換成 FunctionExpression的結(jié)構(gòu),但是需要我們組裝類似的結(jié)構(gòu),這么直接寫很麻煩,但是 babel 為我們提供了一個工具叫做 @babel/types

          @babel/types 有兩個作用:

          1. 判斷這個節(jié)點是不是這個節(jié)點(ArrowFunctionExpression 下面的 path.node 是不是一個 ArrowFunctionExpression)
          2. 生成對應的表達式

          然后我們使用的時候,需要經(jīng)常查文檔,因為里面的節(jié)點類型特別多,不是做編譯相關(guān)工作的是記不住怎么多節(jié)點的

          那么接下來我們就開始生成一個 FunctionExpression,然后把之前的 ArrowFunctionExpression 替換掉,我們可以看 types 文檔,找到 functionExpression,該方法接受相應的參數(shù)我們傳遞過去即可生成一個 FunctionExpression

          t.functionExpression(id,?params,?body,?generator,?async)
          • id: Identifier (default: null) id 可傳遞 null
          • params: Array (required) 函數(shù)參數(shù),可以把之前的參數(shù)拿過來
          • body: BlockStatement (required) 函數(shù)體,接受一個 BlockStatement 我們需要生成一個
          • generator: boolean (default: false) 是否為 generator 函數(shù),當然不是了
          • async: boolean (default: false) 是否為 async 函數(shù),肯定不是了

          還需要生成一個 BlockStatement,我們接著看文檔找到 BlockStatement 接受的參數(shù)

          t.blockStatement(body,?directives)

          看文檔說明,blockStatement 接受一個 body,那我們把之前的 body 拿過來就可以直接用,不過這里 body 接受一個數(shù)組

          我們細看 AST 結(jié)構(gòu),函數(shù)表達式中的 BlockStatement 中的 body 是一個 ReturnStatement,所以我們還需要生成一個 ReturnStatement

          現(xiàn)在我們就可以改寫 AST 了

          const?babel?=?require('@babel/core')
          const?t?=?require('@babel/types')
          const?code?=?`const?fn?=?(a,?b)?=>?a?+?b`?//?const?fn?=?function(a,?b)?{?return?a?+?b?}
          const?arrowFnPlugin?=?{
          ??//?訪問者模式
          ??visitor:?{
          ????//?當訪問到某個路徑的時候進行匹配
          ????ArrowFunctionExpression(path)?{
          ??????//?拿到節(jié)點然后替換節(jié)點
          ??????const?node?=?path.node
          ??????console.log('ArrowFunctionExpression?->?node',?node)
          ??????//?拿到函數(shù)的參數(shù)
          ??????const?params?=?node.params
          ??????const?body?=?node.body
          ??????const?functionExpression?=?t.functionExpression(null,?params,?t.blockStatement([body]))
          ??????//?替換原來的函數(shù)
          ??????path.replaceWith(functionExpression)
          ????},
          ??},
          }
          const?r?=?babel.transform(code,?{
          ??plugins:?[arrowFnPlugin],
          })
          console.log(r.code)?//?const?fn?=?function?(a,?b)?{?return?a?+?b;?};

          特殊情況

          我們知道在剪頭函數(shù)中是可以省略 return 關(guān)鍵字,我們上面是處理了省略關(guān)鍵字的寫法,但是如果用戶寫了 return 關(guān)鍵字后,我們寫的這個插件就有問題了,所以我們可以在優(yōu)化一下

          const fn = (a, b) => { retrun a + b } -> const fn = function(a, b) { return a + b }

          觀察代碼我們發(fā)現(xiàn),我們就不需要把 body 轉(zhuǎn)換成 blockStatement 了,直接放過去就可以了,那么我們就可以這么寫

          ArrowFunctionExpression(path)?{
          ??//?拿到節(jié)點然后替換節(jié)點
          ??const?node?=?path.node
          ??console.log("ArrowFunctionExpression?->?node",?node)
          ??//?拿到函數(shù)的參數(shù)
          ??const?params?=?node.params
          ??let?body?=?node.body
          ??//?判斷是不是?blockStatement,不是的話讓他變成?blockStatement
          ??if?(!t.isBlockStatement(body))?{
          ????body?=?t.blockStatement([body])
          ??}
          ??const?functionExpression?=?t.functionExpression(null,?params,?body)
          ??//?替換原來的函數(shù)
          ??path.replaceWith(functionExpression)
          }

          按需引入

          在開發(fā)中,我們引入 UI 框架,比如 vue 中用到的 element-uivant 或者 React 中的 antd 都支持全局引入和按需引入,默認是全局引入,如果需要按需引入就需要安裝一個 babel-plugin-import 的插件,將全局的寫法變成按需引入的寫法。

          就拿我最近開發(fā)移動端用的 vant 為例, import { Button } from 'vant' 這種寫法經(jīng)過這個插件之后會變成 import Button from 'vant/lib/Button' 這種寫法,引用整個 vant 變成了我只用了 vant 下面的某一個文件,打包后的文件會比全部引入的文件大小要小很多

          分析語法樹

          import { Button, Icon } from 'vant' 寫法轉(zhuǎn)換為 import Button from 'vant/lib/Button'; import Icon from 'vant/lib/Icon'

          看一下兩個語法樹的區(qū)別

          根據(jù)兩張圖分析我們可以得到一些信息:

          1. 我們發(fā)現(xiàn)解構(gòu)方式引入的模塊只有 import 聲明,第二張圖是兩個 import 聲明
          2. 解構(gòu)方式引入的詳細說明里面(specifiers)是兩個 ImportSpecifier,第二張圖里面是分開的,而且都是 ImportDefaultSpecifier
          3. 他們引入的 source 也不一樣
          4. 那我們要做的其實就是要把單個的 ImportDeclaration 變成多個 ImportDeclaration, 然后把單個 import 解構(gòu)引入的 specifiers 部分 ImportSpecifier 轉(zhuǎn)換成多個 ImportDefaultSpecifier 并修改對應的 source 即可

          分析類型

          為了方便傳遞參數(shù),這次我們寫到一個函數(shù)里面,可以方便傳遞轉(zhuǎn)換后拼接的目錄

          這里我們需要用到的幾個類型,也需要在 types 官網(wǎng)上找對應的解釋

          • 首先我們要生成多個 importDeclaration 類型

            /**
            ?*?@param?{Array}?specifiers??(required)
            ?*?@param?{StringLiteral}?source?(required)
            ?*/

            t.importDeclaration(specifiers,?source)
          • importDeclaration 中需要生成 ImportDefaultSpecifier

            /**
            ?*?@param?{Identifier}?local??(required)
            ?*/

            t.importDefaultSpecifier(local)
          • importDeclaration 中還需要生成一個 StringLiteral

            /**
            ?*?@param?{string}?value??(required)
            ?*/

            t.stringLiteral(value)

          上代碼

          按照上面的分析,我們開始上代碼

          const?babel?=?require('@babel/core')
          const?t?=?require('@babel/types')
          const?code?=?`import?{?Button,?Icon?}?from?'vant'`
          //?import?Button?from?'vant/lib/Button'
          //?import?Icon?from?'vant/lib/Icon'
          function?importPlugin(opt)?{
          ??const?{?libraryDir?}?=?opt
          ??return?{
          ????visitor:?{
          ??????ImportDeclaration(path)?{
          ????????const?node?=?path.node
          ????????//?console.log("ImportDeclaration?->?node",?node)
          ????????//?得到節(jié)點的詳細說明,然后轉(zhuǎn)換成多個的?import?聲明
          ????????const?specifiers?=?node.specifiers
          ????????//?要處理這個我們做一些判斷,首先判斷不是默認導出我們才處理,要考慮?import?vant,?{?Button,?Icon?}?from?'vant'?寫法
          ????????//?還要考慮?specifiers?的長度,如果長度不是?1?并且不是默認導出我們才需要轉(zhuǎn)換
          ????????if?(!(specifiers.length?===?1?&&?t.isImportDefaultSpecifier(specifiers[0])))?{
          ??????????const?result?=?specifiers.map((specifier)?=>?{
          ????????????const?local?=?specifier.local
          ????????????const?source?=?t.stringLiteral(`${node.source.value}/${libraryDir}/${specifier.local.name}`)
          ????????????//?console.log("ImportDeclaration?->?specifier",?specifier)
          ????????????return?t.importDeclaration([t.importDefaultSpecifier(local)],source)
          ??????????})
          ??????????console.log('ImportDeclaration?->?result',?result)
          ??????????//?因為這次要替換的?AST?不是一個,而是多個的,所以需要?`path.replaceWithMultiple(result)`?來替換,但是一執(zhí)行發(fā)現(xiàn)死循環(huán)了
          ??????????path.replaceWithMultiple(result)
          ????????}
          ??????},
          ????},
          ??}
          }
          const?r?=?babel.transform(code,?{
          ??plugins:?[importPlugin({?libraryDir:?'lib'?})],
          })
          console.log(r.code)

          看打印結(jié)果和轉(zhuǎn)換結(jié)果似乎沒什么問題,這個插件幾乎就實現(xiàn)了

          特殊情況

          但是我們考慮一種情況,如果用戶不全部按需加載了,按需加載只是一種選擇,如果用戶這么寫了 import vant, { Button, Icon } from 'vant',那么我們這個插件就出現(xiàn)問題了


          如果遇到這種寫法,那么默認導入的他的 source 應該是不變的,我們要把原來的 source 拿出來

          所以還需要判斷一下,每一個 specifier 是不是一個 ImportDefaultSpecifier 然后處理不同的 source,完整處理邏輯應該如下

          function?importPlugin(opt)?{
          ??const?{?libraryDir?}?=?opt
          ??return?{
          ????visitor:?{
          ??????ImportDeclaration(path)?{
          ????????const?node?=?path.node
          ????????//?console.log("ImportDeclaration?->?node",?node)
          ????????//?得到節(jié)點的詳細說明,然后轉(zhuǎn)換成多個的?import?聲明
          ????????const?specifiers?=?node.specifiers
          ????????//?要處理這個我們做一些判斷,首先判斷不是默認導出我們才處理,要考慮?import?vant,?{?Button,?Icon?}?from?'vant'?寫法
          ????????//?還要考慮?specifiers?的長度,如果長度不是?1?并且不是默認導出我們才需要轉(zhuǎn)換
          ????????if?(
          ??????????!(
          ????????????specifiers.length?===?1?&&?t.isImportDefaultSpecifier(specifiers[0])
          ??????????)
          ????????)?{
          ??????????const?result?=?specifiers.map((specifier)?=>?{
          ????????????let?local?=?specifier.local,
          ??????????????source
          ????????????//?判斷是否存在默認導出的情況
          ????????????if?(t.isImportDefaultSpecifier(specifier))?{
          ??????????????source?=?t.stringLiteral(node.source.value)
          ????????????}?else?{
          ??????????????source?=?t.stringLiteral(
          ????????????????`${node.source.value}/${libraryDir}/${specifier.local.name}`
          ??????????????)
          ????????????}
          ????????????return?t.importDeclaration(
          ??????????????[t.importDefaultSpecifier(local)],
          ??????????????source
          ????????????)
          ??????????})
          ??????????path.replaceWithMultiple(result)
          ????????}
          ??????},
          ????},
          ??}
          }

          babylon

          在 babel 官網(wǎng)上有一句話 Babylon is a JavaScript parser used in Babel.

          babylon 與 babel 的關(guān)系

          babel 使用的引擎是 babylonBabylon 并非 babel 團隊自己開發(fā)的,而是 fork 的 acorn 項目,acorn 的項目本人在很早之前在興趣部落 1.0 在構(gòu)建中使用,為了是做一些代碼的轉(zhuǎn)換,是很不錯的一款引擎,不過 acorn 引擎只提供基本的解析 ast 的能力,遍歷還需要配套的 acorn-travesal, 替換節(jié)點需要使用 acorn-,而這些開發(fā),在 Babel 的插件體系開發(fā)下,變得一體化了(摘自 AlloyTeam 團隊的剖析 babel)

          使用 babylon

          使用 babylon 編寫一個數(shù)組 rest 轉(zhuǎn) Es5 語法的插件

          const arr = [ ...arr1, ...arr2 ] 轉(zhuǎn)成 var arr = [].concat(arr1, arr2)

          我們使用 babylon 的話就不需要使用 @babel/core 了,只需要用到他里面的 traversegenerator,用到的包有 babylon、@babel/traverse、@babel/generator、@babel/types

          分析語法樹

          先來看一下兩棵語法樹的區(qū)別

          根據(jù)上圖我們分析得出:

          1. 兩棵樹都是變量聲明的方式,不同的是他們聲明的關(guān)鍵字不一樣
          2. 他們初始化變量值的時候是不一樣的,一個數(shù)組表達式(ArrayExpression)另一個是調(diào)用表達式(CallExpression)
          3. 那我們要做的就很簡單了,就是把 數(shù)組表達式轉(zhuǎn)換為調(diào)用表達式就可以

          分析類型

          這段代碼的核心生成一個 callExpression 調(diào)用表達式,所以對應官網(wǎng)上的類型,我們分析需要用到的 api

          • 先來分析 init 里面的,首先是 callExpression

            /**
            ?*?@param?{Expression}?callee??(required)
            ?*?@param?{Array}?source?(required)
            ?*/

            t.callExpression(callee,?arguments)
          • 對應語法樹上 callee 是一個 MemberExpression,所以要生成一個成員表達式

            /**
            ?*?@param?{Expression}?object??(required)
            ?*?@param?{if?computed?then?Expression?else?Identifier}?property?(required)
            ?*?@param?{boolean}?computed?(default:?false)
            ?*?@param?{boolean}?optional?(default:?null)
            ?*/

            t.memberExpression(object,?property,?computed,?optional)
          • 在 callee 的 object 是一個 ArrayExpression 數(shù)組表達式,是一個空數(shù)組

            /**
            ?*?@param?{Array}?elements??(default:?[])
            ?*/

            t.arrayExpression(elements)
          • 對了里面的東西分析完了,我們還要生成 VariableDeclarator 和 VariableDeclaration 最終生成新的語法樹

            /**
            ?*?@param?{LVal}?id??(required)
            ?*?@param?{Expression}?init?(default:?null)
            ?*/

            t.variableDeclarator(id,?init)

            /**
            ?*?@param?{"var"?|?"let"?|?"const"}?kind??(required)
            ?*?@param?{Array}?declarations?(required)
            ?*/

            t.variableDeclaration(kind,?declarations)
          • 其實倒著分析語法樹,分析完怎么寫也就清晰了,那么我們開始上代碼吧

          上代碼

          const?babylon?=?require('babylon')
          //?使用?babel?提供的包,traverse?和?generator?都是被暴露在?default?對象上的
          const?traverse?=?require('@babel/traverse').default
          const?generator?=?require('@babel/generator').default
          const?t?=?require('@babel/types')

          const?code?=?`const?arr?=?[?...arr1,?...arr2?]`?//?var?arr?=?[].concat(arr1,?arr2)

          const?ast?=?babylon.parse(code,?{
          ??sourceType:?'module',
          })

          //?轉(zhuǎn)換樹
          traverse(ast,?{
          ??VariableDeclaration(path)?{
          ????const?node?=?path.node
          ????const?declarations?=?node.declarations
          ????console.log('VariableDeclarator?->?declarations',?declarations)
          ????const?kind?=?'var'
          ????//?邊界判定
          ????if?(node.kind?!==?kind?&&?declarations.length?===?1?&&?t.isArrayExpression(declarations[0].init))?{
          ??????//?取得之前的?elements
          ??????const?args?=?declarations[0].init.elements.map((item)?=>?item.argument)
          ??????const?callee?=?t.memberExpression(t.arrayExpression(),?t.identifier('concat'),?false)
          ??????const?init?=?t.callExpression(callee,?args)
          ??????const?declaration?=?t.variableDeclarator(declarations[0].id,?init)
          ??????const?variableDeclaration?=?t.variableDeclaration(kind,?[declaration])
          ??????path.replaceWith(variableDeclaration)
          ????}
          ??},
          })

          具體語法書

          和抽象語法樹相對的是具體語法樹(Concrete Syntax Tree)簡稱 CST(通常稱作分析樹)。一般的,在源代碼的翻譯和編譯過程中,語法分析器創(chuàng)建出分析樹。一旦 AST 被創(chuàng)建出來,在后續(xù)的處理過程中,比如語義分析階段,會添加一些信息。可參考抽象語法樹和具體語法樹有什么區(qū)別?

          補充

          關(guān)于 node 類型,全集大致如下:

          (parameter)?node:?Identifier?|?SimpleLiteral?|?RegExpLiteral?|?Program?|?FunctionDeclaration?|?FunctionExpression?|?ArrowFunctionExpression?|?SwitchCase?|?CatchClause?|?VariableDeclarator?|?ExpressionStatement?|?BlockStatement?|?EmptyStatement?|?DebuggerStatement?|?WithStatement?|?ReturnStatement?|?LabeledStatement?|?BreakStatement?|?ContinueStatement?|?IfStatement?|?SwitchStatement?|?ThrowStatement?|?TryStatement?|?WhileStatement?|?DoWhileStatement?|?ForStatement?|?ForInStatement?|?ForOfStatement?|?VariableDeclaration?|?ClassDeclaration?|?ThisExpression?|?ArrayExpression?|?ObjectExpression?|?YieldExpression?|?UnaryExpression?|?UpdateExpression?|?BinaryExpression?|?AssignmentExpression?|?LogicalExpression?|?MemberExpression?|?ConditionalExpression?|?SimpleCallExpression?|?NewExpression?|?SequenceExpression?|?TemplateLiteral?|?TaggedTemplateExpression?|?ClassExpression?|?MetaProperty?|?AwaitExpression?|?Property?|?AssignmentProperty?|?Super?|?TemplateElement?|?SpreadElement?|?ObjectPattern?|?ArrayPattern?|?RestElement?|?AssignmentPattern?|?ClassBody?|?MethodDefinition?|?ImportDeclaration?|?ExportNamedDeclaration?|?ExportDefaultDeclaration?|?ExportAllDeclaration?|?ImportSpecifier?|?ImportDefaultSpecifier?|?ImportNamespaceSpecifier?|?ExportSpecifier

          Babel 有文檔對 AST 樹的詳細定義,可參考這里

          配套源碼地址

          代碼以存放到 GitHub,地址:https://github.com/fecym/ast-share

          參考鏈接

          1. JavaScript 語法解析、AST、V8、JIT
          2. 詳解 AST 抽象語法樹
          3. AST 抽象語法樹 ps: 這個里面有 class 轉(zhuǎn) Es5 構(gòu)造函數(shù)的過程,有興趣可以看一下
          4. 剖析 Babel——Babel 總覽 | AlloyTeam
          5. @babel/types

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7. 80+篇原創(chuàng)系列匯總

          回復“加群”與大佬們一起交流學習~

          點擊“閱讀原文”查看 80+ 篇原創(chuàng)文章

          瀏覽 63
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲av电影网 | 欧美乱伦精品 | 99大香蕉| 超碰人操 | а√天堂资源官网在线资源 |