软件简介
Esprima 是一个用于教育目的的 ECMAScript(JavaScript) 解析架构,主要用于多用途分析。其本身也是使用 ECMAScript 编写的。
主要特性
- 支持 ECMAScript 5.1
- 抽象语法树 (AST) 敏感的格式,兼容 Mozilla Parser API
- 经过重度测试,超过 500 个单元测试以及 100% 的代码覆盖
- 可选跟踪语法节点定位 (index-based and line-column)
- 超级快,速度是 UglifyJS
parse-js
的 2.5 倍(speed comparison)
基础扫盲
- Esprima 是一个用于对 JS 代码做词法或者语法分析的工具
- 体验网址
- 只支持js,不支持 flow 或者 typescript 格式
- 当前最新版本是4.0,主要提供两个API:
- parseScript 解析不包含 import 和 export 之类的js 代码
- parseModule 解析含 import 和 export 之类的js 代码
- 4.0 以下的版本仅支持 parse 方法,需自行判断是 script 还是 module
- 语法格式
esprima.parseScript(input, config, delegate)
esprima.parseModule(input, config, delegate)
input 代表原始 js 字符串
config 是如下的配置对象:
config
delegate参数
// node 包含节点类型等信息,metadata 包含该节点位置等信息
function (node, metadata) {
console.log(node.type, metadata);
}
进阶
Esprima 是用来做词法和语法分析的,这需要对其解析之后的对象结构有清楚的了解,本节分析 Esprima 解析后生成的语法结构树。
总体结构
语法树的总体结构就两种
interface Program {
type: 'Program';
sourceType: 'script';
body: StatementListItem[];
}
interface Program {
type: 'Program';
sourceType: 'module';
body: ModuleItem[];
}
StatementListItem && ModuleItem
其中 ModuleItem(模块项)只是比 StatementListItem(变量声明和执行语句列表项)多了导入和导出两个module才会用到的类型,这两个类型用的少,所以只用关心 StatementListItem
type StatementListItem = Declaration | Statement;
type ModuleItem = ImportDeclaration | ExportDeclaration | StatementListItem;
从 StatementListItem 可看出其只包含 Declaration(变量声明) 和 Statement(执行语句)
枚举 Declaration
type Declaration = ClassDeclaration | FunctionDeclaration | VariableDeclaration;
声明包括:类声明、函数声明、变量声明
枚举 Statement
type Statement = BlockStatement | BreakStatement | ContinueStatement |
DebuggerStatement | DoWhileStatement | EmptyStatement |
ExpressionStatement | ForStatement | ForInStatement |
ForOfStatement | FunctionDeclaration | IfStatement |
LabeledStatement | ReturnStatement | SwitchStatement |
ThrowStatement | TryStatement | VariableDeclaration |
WhileStatement | WithStatement;
执行语句包括:块、break、continue、debugger、do while、空语句、表达式语句、for、for in、for of、function、if、标签、return、switch、throw、try、var、while、with。
其中 ExpressionStatement 比较复杂
interface ExpressionStatement {
type: 'ExpressionStatement';
expression: Expression;
directive?: string;
}
// Expression 类型
type Expression = ThisExpression | Identifier | Literal |
ArrayExpression | ObjectExpression | FunctionExpression | ArrowFunctionExpression | ClassExpression |
TaggedTemplateExpression | MemberExpression | Super | MetaProperty |
NewExpression | CallExpression | UpdateExpression | AwaitExpression | UnaryExpression |
BinaryExpression | LogicalExpression | ConditionalExpression |
YieldExpression | AssignmentExpression | SequenceExpression;
小结
Esprima 本质上将 js 代码解析成了两大部分:
- 3 种变量声明(函数、变量和类)
- 表达式
其中表达式又被分为了两大类:
- 关键字组成的 statement,如 IfStatement, ForStatement等,这里面的BlockStatement有些特殊,因为其body又是 StatementListItem,产生递归。
- 运算语句(赋值、计算之类的操作)组成的 ExpressionStatement
看个例子:
// 解析
var answer = 6 * 7;
if(true){answer =1}
// 结果
{
"type": "Program",
"sourceType": "script",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"kind": "var"
},
{
"type": "IfStatement",
"test": {
"type": "Literal",
"value": true,
"raw": "true"
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "answer"
},
"right": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
}
]
},
"alternate": null
}
]
}
应用案例
去除 console.log() 语句,主要利用了 delegate 的第二个参数获取 console.log() 语句的位置,然后做字符串拼接
const esprima = require('esprima');
// console.log(x) or console['error'](y)
function isConsoleCall(node) {
return (node.type === 'CallExpression') &&
(node.callee.type === 'MemberExpression') &&
(node.callee.object.type === 'Identifier') &&
(node.callee.object.name === 'console');
}
function removeCalls(source) {
const entries = [];
esprima.parseScript(source, {}, function (node, meta) {
if (isConsoleCall(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset
});
}
});
entries.sort((a, b) => { return b.end - a.end }).forEach(n => {
source = source.slice(0, n.start) + source.slice(n.end);
});
return source;
}
语法结构说明
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
后边要执行的语句,一般会是一个块语句。
1 2 3 4 5 |
|
下边是控制流的语句:
ReturnStatement
返回语句节点,argument
属性是一个表达式,代表返回的内容。
1 2 3 4 |
|
LabeledStatement
label
语句,平时可能会比较少接触到,举个例子:
1 2 3 4 5 6 7 |
|
这里的 loop
就是一个 label
了,我们可以在循环嵌套中使用 break loop
来指定跳出哪个循环。所以这里的 label
语句指的就是 loop: ...
这个。
一个 label
语句节点会有两个属性,一个 label
属性表示 label
的名称,另外一个 body
属性指向对应的语句,通常是一个循环语句或者 switch
语句。
1 2 3 4 5 |
|
BreakStatement
break
语句节点,会有一个 label
属性表示需要的 label
名称,当不需要 label
的时候(通常都不需要),便是 null
。
1 2 3 4 |
|
ContinueStatement
continue
语句节点,和 break
类似。
1 2 3 4 |
|
下边是条件语句:
IfStatement
if
语句节点,很常见,会带有三个属性,test
属性表示 if (...)
括号中的表达式。
consequent
属性是表示条件为 true
时的执行语句,通常会是一个块语句。
alternate
(候补,交替)属性则是用来表示 else
后跟随的语句节点,通常也会是块语句,但也可以又是一个 if
语句节点,即类似这样的结构: if (a) { //... } else if (b) { // ... }
。 alternate
当然也可以为 null
。
1 2 3 4 5 6 |
|
SwitchStatement
switch
语句节点,有两个属性,discriminant
属性表示 switch
语句后紧随的表达式,通常会是一个变量,cases
属性是一个 case
节点的数组,用来表示各个 case
语句。
1 2 3 4 5 |
|
SwitchCase
switch
的 case
节点。test
属性代表这个 case
的判断表达式,consequent
则是这个 case
的执行语句。
当 test
属性是 null
时,则是表示 default
这个 case
节点。
1 2 3 4 5 |
|
下边是异常相关的语句:
ThrowStatement
throw
语句节点,argument
属性用以表示 throw
后边紧跟的表达式。
1 2 3 4 |
|
TryStatement
try
语句节点,block
属性表示 try
的执行语句,通常是一个块语句。
hanlder
属性是指 catch
节点,finalizer
是指 finally
语句节点,当 hanlder
为 null
时,finalizer
必须是一个块语句节点。
1 2 3 4 5 6 |
|
CatchClause
catch
节点,param
用以表示 catch
后的参数,body
则表示 catch
后的执行语句,通常是一个块语句。
1 2 3 4 5 |
|
下边是循环语句:
WhileStatement
while
语句节点,test
表示括号中的表达式,body
是表示要循环执行的语句。
1 2 3 4 5 |
|
DoWhileStatement
do/while
语句节点,和 while
语句类似。
1 2 3 4 5 |
|
ForStatement
for
循环语句节点,属性 init/test/update
分别表示了 for
语句括号中的三个表达式,初始化值,循环判断条件,每次循环执行的变量更新语句(init
可以是变量声明或者表达式)。这三个属性都可以为 null
,即 for(;;){}
。 body
属性用以表示要循环执行的语句。
1 2 3 4 5 6 7 |
|
ForInStatement
for/in
语句节点,left
和 right
属性分别表示在 in
关键词左右的语句(左侧可以是一个变量声明或者表达式)。body
依旧是表示要循环执行的语句。
1 2 3 4 5 6 |
|
Declarations
声明语句节点,同样也是语句,只是一个类型的细化。下边会介绍各种声明语句类型。
1 |
|
FunctionDeclaration
函数声明,和之前提到的 Function 不同的是,id
不能为 null
。
1 2 3 4 |
|
VariableDeclaration
变量声明,kind
属性表示是什么类型的声明,因为 ES6 引入了 const/let
。declarations
表示声明的多个描述,因为我们可以这样:let a = 1, b = 2;
。
1 2 3 4 5 |
|
VariableDeclarator
变量声明的描述,id
表示变量名称节点,init
表示初始值的表达式,可以为 null
。
1 2 3 4 5 |
|
Expressions
表达式节点。
1 |
|
ThisExpression
表示 this
。
1 2 3 |
|
ArrayExpression
数组表达式节点,elements
属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。
1 2 3 4 |
|
ObjectExpression
对象表达式节点,property
属性是一个数组,表示对象的每一个键值对,每一个元素都是一个属性节点。
1 2 3 4 |
|
Property
对象表达式中的属性节点。key
表示键,value
表示值,由于 ES5 语法中有 get/set
的存在,所以有一个 kind
属性,用来表示是普通的初始化,或者是 get/set
。
1 2 3 4 5 6 |
|
FunctionExpression
函数表达式节点。
1 2 3 |
|
下边是一元运算符相关的表达式部分:
UnaryExpression
一元运算表达式节点(++/--
是 update 运算符,不在这个范畴内),operator
表示运算符,prefix
表示是否为前缀运算符。argument
是要执行运算的表达式。
1 2 3 4 5 6 |
|
UnaryOperator
一元运算符,枚举类型,所有值如下:
1 2 3 |
|
UpdateExpression
update 运算表达式节点,即 ++/--
,和一元运算符类似,只是 operator
指向的节点对象类型不同,这里是 update 运算符。
1 2 3 4 5 6 |
|
UpdateOperator
update 运算符,值为 ++
或 --
,配合 update 表达式节点的 prefix
属性来表示前后。
1 2 3 |
|
下边是二元运算符相关的表达式部分:
BinaryExpression
二元运算表达式节点,left
和 right
表示运算符左右的两个表达式,operator
表示一个二元运算符。
1 2 3 4 5 6 |
|
BinaryOperator
二元运算符,所有值如下:
1 2 3 4 5 6 7 8 |
|
AssignmentExpression
赋值表达式节点,operator
属性表示一个赋值运算符,left
和 right
是赋值运算符左右的表达式。
1 2 3 4 5 6 |
|
AssignmentOperator
赋值运算符,所有值如下:(常用的并不多)
1 2 3 4 5 |
|
LogicalExpression
逻辑运算表达式节点,和赋值或者二元运算类型,只不过 operator
是逻辑运算符类型。
1 2 3 4 5 6 |
|
LogicalOperator
逻辑运算符,两种值,即与或。
1 2 3 |
|
MemberExpression
成员表达式节点,即表示引用对象成员的语句,object
是引用对象的表达式节点,property
是表示属性名称,computed
如果为 false
,是表示 .
来引用成员,property
应该为一个 Identifier
节点,如果 computed
属性为 true
,则是 []
来进行引用,即 property
是一个 Expression
节点,名称是表达式的结果值。
1 2 3 4 5 6 |
|
下边是其他的一些表达式:
ConditionalExpression
条件表达式,通常我们称之为三元运算表达式,即 boolean ? true : false
。属性参考条件语句。
1 2 3 4 5 6 |
|
CallExpression
函数调用表达式,即表示了 func(1, 2)
这一类型的语句。callee
属性是一个表达式节点,表示函数,arguments
是一个数组,元素是表达式节点,表示函数参数列表。
1 2 3 4 5 |
|
NewExpression
new
表达式。
1 2 3 |
|
SequenceExpression
这个就是逗号运算符构建的表达式(不知道确切的名称),expressions
属性为一个数组,即表示构成整个表达式,被逗号分割的多个表达式。
1 2 3 4 |
|
Patterns
模式,主要在 ES6 的解构赋值中有意义,在 ES5 中,可以理解为和 Identifier
差不多的东西。
1 |
|
参考:
https://www.jianshu.com/p/47d9b2a365c5