Most situations can be handled using regular expression matching, but once it depends on the content of the code context, regular or simple character parsing is very inadequate. At this time, a language parser is needed to obtain the entire AST (abstract syntax tree) ).
Then I found multiple JavaScript parsers written in JavaScript:
Esprima
Acorn
UglifyJS 2
Shift
Judging from the submission records, the maintenance status is pretty good, and various ES development features I could keep up with them all, and I had a brief introduction to each of them and talked about some of their situations.
Esprima is a very classic parser. Acorn was born after it, which was a few years ago. According to the author of Acorn, building this wheel was more just for fun. The speed is comparable to Esprima, but the implementation code is less. The key point is that the AST results produced by these two parsers (yes, just the AST, the tokens are different) are in line with The Estree Spec specification (this is the specification document of the JavaScript AST output by the SpiderMonkey engine given by Mozilla engineers) , you can also refer to: SpiderMonkey in MDN), that is, the results obtained are largely compatible.
The now famous Webpack also uses Acorn when parsing code.
As for Uglify, a very famous JavaScript code compressor, it actually comes with a code parser and can also output AST, but its function is more for compressing code. If it is used to parse code, it feels not pure enough.
I don’t know much about Shift. I only know that it defines its own set of AST specifications.
There is a performance test on the Esprima official website. The results I ran on chrome are as follows:
It can be seen that Acorn's performance is very good, and there is also an Estree specification (standards are very important, I personally think that following universal specifications is an important basis for code reuse), so I directly chose Acorn for code parsing.
The performance comparison in the picture is also Google's Traceur, which is more of an ES6 to ES5 compiler and does not match the positioning of the parser we are looking for.
The following. Let’s get to the point, how to use Acorn to parse JavaScript.
API
The API of the parser is very simple:
const ast = acorn.parse(code, options)
Acorn has quite a few configuration items, and it also includes some events that can be set up with callback functions. Let’s talk about a more important one:
ecmaVersion
The literal meaning is easy to understand, which is to set the ECMA version of the JavaScript you want to parse. The default is ES7.
sourceType
This configuration item has two values: module and script. , the default is script.
The main difference is strict mode and import/export. The module in ES6 is strict mode, which means you don’t need to add use strict. The script we usually use in browsers does not have import/export syntax.
So, if you select script and an import/export error occurs, you can use strict mode declaration. If you select module, you do not need strict mode declaration and you can use import/export syntax.
locations
The default value is false and is set to false. After true, an additional loc object will be carried in the AST node to represent the current starting and ending row and column numbers.
onComment
passes in a callback function, which will be triggered whenever a comment in the code is parsed. You can get the current year's comment content. The parameter list is: [block, text, start, end].
block indicates whether it is a block comment, text is the comment content, and start and end are the starting and ending positions of the comment.
mentioned above. Espree requires Esprima's attachComment configuration item. When set to true, Esprima will carry comment-related information (trailingComments and leadingComments) in the nodes of the code parsing results. Espree uses Acorn's onComment configuration to achieve compatibility with this Esprima feature.
The parser usually also has an interface for obtaining lexical analysis results:
const tokens = [...acorn.tokenizer(code, options)]
The second parameter of the tokenizer method can also configure locations.
There are certain differences between the lexical result token and Esprima's result data structure (Espree has made this level of compatibility again). If you are interested in knowing more, you can take a look at Esprima's parsing results: http://esprima.org/demo /parse... .
As for the contents of AST and token parsed by Acorn, we will elaborate next.
Token
I searched for a long time, but couldn’t find a detailed introduction to the token data structure, so I had to take a look at it myself.
The code I used to test the parsing is:
import "hello.js" var a = 2; // test function name() { console.log(arguments); }
The parsed token array is an object similar to this:
Token { type: TokenType { label: 'import', keyword: 'import', beforeExpr: false, startsExpr: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, value: 'import', start: 5, end: 11 },
It seems to be easy to understand, right? In the object corresponding to type, label represents the current identification. A type, keyword is a keyword, like import in the example, or function.
value is the value of the current identifier, and start/end are the start and end positions respectively.
Usually what we need to pay attention to is label/keyword/value. For other details, please refer to the source code: tokentype.js.
The Estree Spec
这一部分是重头戏,因为实际上我需要的还是解析出来的 AST。最原滋原味的内容来自于:The Estree Spec,我只是阅读了之后的搬运工。
提供了标准文档的好处是,很多东西有迹可循,这里还有一个工具,用于把满足 Estree 标准的 AST 转换为 ESMAScript 代码:escodegen。
好吧,回到正题,我们先来看一下 ES5 的部分,可以在 Esprima: Parser 这个页面测试各种代码的解析结果。
符合这个规范的解析出来的 AST 节点用 Node 对象来标识,Node 对象应该符合这样的接口:
interface Node { type: string; loc: SourceLocation | null; }
type 字段表示不同的节点类型,下边会再讲一下各个类型的情况,分别对应了 JavaScript 中的什么语法。
loc 字段表示源码的位置信息,如果没有相关信息的话为 null,否则是一个对象,包含了开始和结束的位置。接口如下:
interface SourceLocation { source: string | null; start: Position; end: Position; }
这里的 Position 对象包含了行和列的信息,行从 1 开始,列从 0 开始:
interface Position { line: number; // >= 1 column: number; // >= 0 }
好了,基础部分就是这样,接下来看各种类型的节点,顺带温习一下 JavaScript 语法的一些东西吧。对于这里每一部分的内容,会简单谈一下,但不会展开(内容不少),对 JavaScript 了解的人很容易就明白的。
我觉得看完就像把 JavaScript 的基础语法整理了一遍。
Identifier
标识符,我觉得应该是这么叫的,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符。相应的接口是这样的:
interface Identifier <: Expression, Pattern { type: "Identifier"; name: string; }
一个标识符可能是一个表达式,或者是解构的模式(ES6 中的解构语法)。我们等会会看到 Expression 和 Pattern 相关的内容的。
Literal
字面量,这里不是指 [] 或者 {} 这些,而是本身语义就代表了一个值的字面量,如 1,“hello”, true 这些,还有正则表达式(有一个扩展的 Node 来表示正则表达式),如 /d?/。我们看一下文档的定义:
interface Literal <: Expression { type: "Literal"; value: string | boolean | null | number | RegExp; }
value 这里即对应了字面量的值,我们可以看出字面量值的类型,字符串,布尔,数值,null 和正则。
RegExpLiteral
这个针对正则字面量的,为了更好地来解析正则表达式的内容,添加多一个 regex 字段,里边会包括正则本身,以及正则的flags。
interface RegExpLiteral <: Literal { regex: { pattern: string; flags: string; }; }
Programs
一般这个是作为跟节点的,即代表了一棵完整的程序代码树。
interface Program <: Node { type: "Program"; body: [ Statement ]; }
body 属性是一个数组,包含了多个 Statement(即语句)节点。
Functions
函数声明或者函数表达式节点。
interface Function <: Node { id: Identifier | null; params: [ Pattern ]; body: BlockStatement; }
id 是函数名,params 属性是一个数组,表示函数的参数。body 是一个块语句。
有一个值得留意的点是,你在测试过程中,是不会找到 type: "Function" 的节点的,但是你可以找到 type: "FunctionDeclaration" 和 type: "FunctionExpression",因为函数要么以声明语句出现,要么以函数表达式出现,都是节点类型的组合类型,后边会再提及 FunctionDeclaration 和 FunctionExpression 的相关内容。
这让人感觉这个文档规划得蛮细致的,函数名,参数和函数块是属于函数部分的内容,而声明或者表达式则有它自己需要的东西。
Statement
语句节点没什么特别的,它只是一个节点,一种区分,但是语句有很多种,下边会详述。
interface Statement <: Node { }
ExpressionStatement
表达式语句节点,a = a + 1 或者 a++ 里边会有一个 expression 属性指向一个表达式节点对象(后边会提及表达式)。
interface ExpressionStatement <: Statement { type: "ExpressionStatement"; expression: Expression; }
BlockStatement
块语句节点,举个例子:if (...) { // 这里是块语句的内容 },块里边可以包含多个其他的语句,所以有一个 body 属性,是一个数组,表示了块里边的多个语句。
interface BlockStatement <: Statement { type: "BlockStatement"; body: [ Statement ]; }
EmptyStatement
一个空的语句节点,没有执行任何有用的代码,例如一个单独的分号 ;
interface EmptyStatement <: Statement { type: "EmptyStatement "; }
DebuggerStatement
debugger,就是表示这个,没有其他了。
interface DebuggerStatement <: Statement { type: "DebuggerStatement"; }
WithStatement
with 语句节点,里边有两个特别的属性,object 表示 with 要使用的那个对象(可以是一个表达式),body 则是对应 with 后边要执行的语句,一般会是一个块语句。
interface WithStatement <: Statement { type: "WithStatement"; object: Expression; body: Statement; }
下边是控制流的语句:
ReturnStatement
返回语句节点,argument 属性是一个表达式,代表返回的内容。
interface ReturnStatement <: Statement { type: "ReturnStatement"; argument: Expression | null; }
LabeledStatement
label 语句,平时可能会比较少接触到,举个例子:
loop: for(let i = 0; i < len; i++) { // ... for (let j = 0; j < min; j++) { // ... break loop; } }
这里的 loop 就是一个 label 了,我们可以在循环嵌套中使用 break loop 来指定跳出哪个循环。所以这里的 label 语句指的就是loop: ... 这个。
一个 label 语句节点会有两个属性,一个 label 属性表示 label 的名称,另外一个 body 属性指向对应的语句,通常是一个循环语句或者 switch 语句。
interface LabeledStatement <: Statement { type: "LabeledStatement"; label: Identifier; body: Statement; }
BreakStatement
break 语句节点,会有一个 label 属性表示需要的 label 名称,当不需要 label 的时候(通常都不需要),便是 null。
interface BreakStatement <: Statement { type: "BreakStatement"; label: Identifier | null; }
ContinueStatement
continue 语句节点,和 break 类似。
interface ContinueStatement <: Statement { type: "ContinueStatement"; label: Identifier | null; }
下边是条件语句:
IfStatement
if 语句节点,很常见,会带有三个属性,test 属性表示 if (...) 括号中的表达式。
consequent 属性是表示条件为 true 时的执行语句,通常会是一个块语句。
alternate 属性则是用来表示 else 后跟随的语句节点,通常也会是块语句,但也可以又是一个 if 语句节点,即类似这样的结构:
if (a) { //... } else if (b) { // ... }。
alternate 当然也可以为 null。
interface IfStatement <: Statement { type: "IfStatement"; test: Expression; consequent: Statement; alternate: Statement | null; }
SwitchStatement
switch 语句节点,有两个属性,discriminant 属性表示 switch 语句后紧随的表达式,通常会是一个变量,cases 属性是一个case 节点的数组,用来表示各个 case 语句。
interface SwitchStatement <: Statement { type: "SwitchStatement"; discriminant: Expression; cases: [ SwitchCase ]; }
SwitchCase
switch 的 case 节点。test 属性代表这个 case 的判断表达式,consequent 则是这个 case 的执行语句。
当 test 属性是 null 时,则是表示 default 这个 case 节点。
interface SwitchCase <: Node { type: "SwitchCase"; test: Expression | null; consequent: [ Statement ]; }
下边是异常相关的语句:
ThrowStatement
throw 语句节点,argument 属性用以表示 throw 后边紧跟的表达式。
interface ThrowStatement <: Statement { type: "ThrowStatement"; argument: Expression; }
TryStatement
try 语句节点,block 属性表示 try 的执行语句,通常是一个块语句。
hanlder 属性是指 catch 节点,finalizer 是指 finally 语句节点,当 hanlder 为 null 时,finalizer 必须是一个块语句节点。
interface TryStatement <: Statement { type: "TryStatement"; block: BlockStatement; handler: CatchClause | null; finalizer: BlockStatement | null; }
CatchClause
catch 节点,param 用以表示 catch 后的参数,body 则表示 catch 后的执行语句,通常是一个块语句。
interface CatchClause <: Node { type: "CatchClause"; param: Pattern; body: BlockStatement; }
下边是循环语句:
WhileStatement
while 语句节点,test 表示括号中的表达式,body 是表示要循环执行的语句。
interface WhileStatement <: Statement { type: "WhileStatement"; test: Expression; body: Statement; }
DoWhileStatement
do/while 语句节点,和 while 语句类似。
interface DoWhileStatement <: Statement { type: "DoWhileStatement"; body: Statement; test: Expression; }
ForStatement
for 循环语句节点,属性 init/test/update 分别表示了 for 语句括号中的三个表达式,初始化值,循环判断条件,每次循环执行的变量更新语句(init 可以是变量声明或者表达式)。这三个属性都可以为 null,即 for(;;){}。
body 属性用以表示要循环执行的语句。
interface ForStatement <: Statement { type: "ForStatement"; init: VariableDeclaration | Expression | null; test: Expression | null; update: Expression | null; body: Statement; }
ForInStatement
for/in 语句节点,left 和 right 属性分别表示在 in 关键词左右的语句(左侧可以是一个变量声明或者表达式)。body 依旧是表示要循环执行的语句。
interface ForInStatement <: Statement { type: "ForInStatement"; left: VariableDeclaration | Pattern; right: Expression; body: Statement; }
Declarations
声明语句节点,同样也是语句,只是一个类型的细化。下边会介绍各种声明语句类型。
interface Declaration <: Statement { }
FunctionDeclaration
函数声明,和之前提到的 Function 不同的是,id 不能为 null。
interface FunctionDeclaration <: Function, Declaration { type: "FunctionDeclaration"; id: Identifier; }
VariableDeclaration
变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。
declarations 表示声明的多个描述,因为我们可以这样:let a = 1, b = 2;。
interface VariableDeclaration <: Declaration { type: "VariableDeclaration"; declarations: [ VariableDeclarator ]; kind: "var"; }
VariableDeclarator
变量声明的描述,id 表示变量名称节点,init 表示初始值的表达式,可以为 null。
interface VariableDeclarator <: Node { type: "VariableDeclarator"; id: Pattern; init: Expression | null; }
Expressions
表达式节点。
interface Expression <: Node { }
ThisExpression
表示 this。
interface ThisExpression <: Expression { type: "ThisExpression"; }
ArrayExpression
数组表达式节点,elements 属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。
interface ArrayExpression <: Expression { type: "ArrayExpression"; elements: [ Expression | null ]; }
ObjectExpression
对象表达式节点,property 属性是一个数组,表示对象的每一个键值对,每一个元素都是一个属性节点。
interface ObjectExpression <: Expression { type: "ObjectExpression"; properties: [ Property ]; }
Property
对象表达式中的属性节点。key 表示键,value 表示值,由于 ES5 语法中有 get/set 的存在,所以有一个 kind 属性,用来表示是普通的初始化,或者是 get/set。
interface Property <: Node { type: "Property"; key: Literal | Identifier; value: Expression; kind: "init" | "get" | "set"; }
FunctionExpression
函数表达式节点。
interface FunctionExpression <: Function, Expression { type: "FunctionExpression"; }
下边是一元运算符相关的表达式部分:
UnaryExpression
一元运算表达式节点(++/-- 是 update 运算符,不在这个范畴内),operator 表示运算符,prefix 表示是否为前缀运算符。argument 是要执行运算的表达式。
interface UnaryExpression <: Expression { type: "UnaryExpression"; operator: UnaryOperator; prefix: boolean; argument: Expression; }
UnaryOperator
一元运算符,枚举类型,所有值如下:
enum UnaryOperator { "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" }
UpdateExpression
update 运算表达式节点,即 ++/--,和一元运算符类似,只是 operator 指向的节点对象类型不同,这里是 update 运算符。
interface UpdateExpression <: Expression { type: "UpdateExpression"; operator: UpdateOperator; argument: Expression; prefix: boolean; }
UpdateOperator
update 运算符,值为 ++ 或 --,配合 update 表达式节点的 prefix 属性来表示前后。
enum UpdateOperator { "++" | "--" }
下边是二元运算符相关的表达式部分:
BinaryExpression
二元运算表达式节点,left 和 right 表示运算符左右的两个表达式,operator 表示一个二元运算符。
interface BinaryExpression <: Expression { type: "BinaryExpression"; operator: BinaryOperator; left: Expression; right: Expression; }
BinaryOperator
二元运算符,所有值如下:
enum BinaryOperator { "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" }
AssignmentExpression
赋值表达式节点,operator 属性表示一个赋值运算符,left 和 right 是赋值运算符左右的表达式。
interface AssignmentExpression <: Expression { type: "AssignmentExpression"; operator: AssignmentOperator; left: Pattern | Expression; right: Expression; }
AssignmentOperator
赋值运算符,所有值如下:(常用的并不多)
enum AssignmentOperator { "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" }
LogicalExpression
逻辑运算表达式节点,和赋值或者二元运算类型,只不过 operator 是逻辑运算符类型。
interface LogicalExpression <: Expression { type: "LogicalExpression"; operator: LogicalOperator; left: Expression; right: Expression; }
LogicalOperator
逻辑运算符,两种值,即与或。
enum LogicalOperator { "||" | "&&" }
MemberExpression
成员表达式节点,即表示引用对象成员的语句,object 是引用对象的表达式节点,property 是表示属性名称,computed 如果为false,是表示 . 来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即property 是一个 Expression 节点,名称是表达式的结果值。
interface MemberExpression <: Expression, Pattern { type: "MemberExpression"; object: Expression; property: Expression; computed: boolean; }
下边是其他的一些表达式:
ConditionalExpression
条件表达式,通常我们称之为三元运算表达式,即 boolean ? true : false。属性参考条件语句。
interface ConditionalExpression <: Expression { type: "ConditionalExpression"; test: Expression; alternate: Expression; consequent: Expression; }
CallExpression
函数调用表达式,即表示了 func(1, 2) 这一类型的语句。callee 属性是一个表达式节点,表示函数,arguments 是一个数组,元素是表达式节点,表示函数参数列表。
interface CallExpression <: Expression { type: "CallExpression"; callee: Expression; arguments: [ Expression ]; }
NewExpression
new 表达式
interface NewExpression <: CallExpression { type: "NewExpression"; }
SequenceExpression
这个就是逗号运算符构建的表达式(不知道确切的名称),expressions 属性为一个数组,即表示构成整个表达式,被逗号分割的多个表达式。
interface SequenceExpression <: Expression { type: "SequenceExpression"; expressions: [ Expression ]; }
Patterns
模式,主要在 ES6 的解构赋值中有意义,在 ES5 中,可以理解为和 Identifier 差不多的东西。
interface Pattern <: Node { }
这一部分的内容比较多,但都可以举一反三,写这个的时候我就当把 JavaScript 语法再复习一遍。这个文档还有 ES2015,ES2016,ES2017 相关的内容,涉及的东西也蛮多,但是理解了上边的这一些,然后从语法层面去思考这个文档,其他的内容也就很好理解了,这里略去,有需要请参阅:The Estree Spec。
Plugins
回到我们的主角,Acorn,提供了一种扩展的方式来编写相关的插件:Acorn Plugins。
我们可以使用插件来扩展解析器,来解析更多的一些语法,如 .jsx 语法,有兴趣的看看这个插件:acorn-jsx。
官方表示 Acorn 的插件是用于方便扩展解析器,但是需要对 Acorn 内部的运行极致比较了解,扩展的方式会在原本的基础上重新定义一些方法。这里不展开讲了,如果我需要插件的话,会再写文章聊聊这个东西。
Examples
现在我们来看一下如何应用这个解析器,例如我们需要用来解析出一个符合 CommonJS 规范的模块依赖了哪些模块,我们可以用 Acorn 来解析 require 这个函数的调用,然后取出调用时的传入参数,便可以获取依赖的模块。
下边是示例代码:
// 遍历所有节点的函数 function walkNode(node, callback) { callback(node) // 有 type 字段的我们认为是一个节点 Object.keys(node).forEach((key) => { const item = node[key] if (Array.isArray(item)) { item.forEach((sub) => { sub.type && walkNode(sub, callback) }) } item && item.type && walkNode(item, callback) }) } function parseDependencies(str) { const ast = acorn.parse(str, { ranges: true }) const resource = [] // 依赖列表 // 从根节点开始 walkNode(ast, (node) => { const callee = node.callee const args = node.arguments // require 我们认为是一个函数调用,并且函数名为 require,参数只有一个,且必须是字面量 if ( node.type === 'CallExpression' && callee.type === 'Identifier' && callee.name === 'require' && args.length === 1 && args[0].type === 'Literal' ) { const args = node.arguments // 获取依赖的相关信息 resource.push({ string: str.substring(node.range[0], node.range[1]), path: args[0].value, start: node.range[0], end: node.range[1] }) } }) return resource }
这只是简单的一个情况的处理,但是已经给我们呈现了如何使用解析器,Webpack 则在这个的基础上做了更多的东西,包括 var r = require; r('a') 或者 require.async('a') 等的处理。
AST 这个东西对于前端来说,我们无时无刻不在享受着它带来的成果(模块构建,代码压缩,代码混淆),所以了解一下总归有好处。