대부분의 상황은 정규식 일치를 사용하여 처리할 수 있지만 일단 코드 컨텍스트의 내용에 따라 일반 또는 단순 문자 구문 분석은 매우 부적절합니다. 이 때 전체 AST(추상)를 얻으려면 언어 구문 분석기가 필요합니다. 구문 트리).
그런 다음 JavaScript로 작성된 여러 JavaScript 파서를 발견했습니다.
Esprima
Acorn
UglifyJS 2
Shift
제출 기록으로 볼 때 유지 관리 상황은 꽤 괜찮고, ES의 다양한 개발 기능도 그에 맞춰 간략히 알아보고 몇 가지 상황에 대해 이야기해 봤습니다.
Esprima는 몇 년 전에 탄생한 매우 고전적인 파서입니다. Acorn의 저자에 따르면 당시 이 바퀴를 만드는 것은 단지 재미를 위해서였다고 합니다. 속도는 Esprima와 비슷하지만 구현 코드가 더 적습니다. 핵심은 이 두 파서(예, AST만, 토큰이 다름)에 의해 생성된 AST 결과가 Estree 사양 사양과 일치한다는 것입니다(이것은 SpiderMonkey 엔진의 JavaScript AST 출력에 대한 사양 문서입니다. Mozilla 엔지니어), MDN의 SpiderMonkey)를 참조할 수도 있습니다. 즉, 얻은 결과는 대체로 호환됩니다.
현재 유명해진 Webpack도 코드를 파싱할 때 Acorn을 사용합니다.
매우 유명한 JavaScript 코드 압축기인 Uglify의 경우 실제로 코드 파서가 함께 제공되며 AST도 출력할 수 있지만 해당 기능은 코드를 압축하는 데 사용되는 경우에는 그렇지 않습니다. 충분히 순수하다고 느껴보세요.
저는 Shift에 대해 잘 모릅니다. Shift가 자체 AST 사양 세트를 정의한다는 것만 알고 있습니다.
Esprima 공식 홈페이지에 성능 테스트가 있습니다. 크롬에서 실행한 결과는 다음과 같습니다.
에이콘의 성능이 매우 좋다고 볼 수 있으며, 에스트리 사양도 있습니다(사양은 매우 중요합니다. 개인적으로는 보편적인 규격을 따른다고 생각합니다. 사양은 코드 재사용의 중요한 기반입니다. 그래서 코드 분석을 위해 Acorn을 직접 선택했습니다.
그림의 성능 비교는 Google의 Traceur로, 그 이상입니다. ES6에서 ES5까지의 컴파일러는 우리가 찾고 있는 파서의 위치와 일치하지 않습니다.
Acorn을 사용하여
API
파서의 API는 매우 복잡합니다.
const ast = acorn.parse(code, options)
Acorn에는 콜백 함수로 설정할 수 있는 일부 이벤트를 포함하여 꽤 많은 구성 항목이 있습니다. 중요한 것:
ecmaVersion<🎜.>
파싱하려는 JavaScript의 ECMA 버전을 설정하는 문자 그대로의 의미는<🎜입니다. >sourceType
이 구성 항목에는 모듈과 스크립트라는 두 가지 값이 있으며, 기본값은 스크립트입니다.
엄격 모드와 가져오기/내보내기의 주요 차이점은 엄격 모드입니다. 이는 use strict를 추가할 필요가 없다는 의미입니다. 우리가 일반적으로 브라우저에서 사용하는 스크립트에는 import/.export 구문이 없습니다.
따라서 스크립트를 선택하고 import/export하면 오류가 보고됩니다. 엄격 모드 선언을 사용할 수 있습니다. 모듈을 선택하면 엄격 모드 선언이 필요하지 않으며 가져오기/내보내기 구문을 사용할 수 있습니다. 🎜>locations
기본값은 false입니다. 객체는 현재 시작 및 끝 행과 열 번호를 나타내기 위해 AST 노드에 전달됩니다.
onComment
코드에 주석이 있을 때마다 트리거되는 콜백 함수를 전달합니다. 현재 연도의 코멘트 내용을 얻을 수 있습니다. 매개변수 목록은 [block, text, start, end]입니다. block은 블록 코멘트인지 여부를 나타내며, text는 코멘트 내용이고, start와 end는 시작입니다.
위에서 언급한 Espree에는 Esprima의 attachmentComment 구성 항목이 필요합니다. 이 항목을 true로 설정하면 Esprima는 코드 구문 분석 결과의 노드에 주석 관련 정보(trailingComments 및leadingComments)를 전달합니다. ). Espree는 Acorn의 onComment 구성을 사용하여 Esprima 기능과의 호환성을 달성합니다.
파서에는 일반적으로 어휘 분석 결과를 얻기 위한 인터페이스도 있습니다.
토큰나이저 방법의 두 번째 매개변수도 위치를 구성할 수 있습니다. 어휘 결과 토큰과 Esprima 결과의 데이터 구조에는 특정 차이점이 있습니다(Espree는 이러한 호환성 계층을 다시 만들었습니다). 이해에 관심이 있다면 Esprima의 구문 분석 결과를 살펴보세요. http://esprima.org/demo/parse… Acorn에서 파싱한 AST와 토큰의 내용에 대해서는 다음에 자세히 설명하겠습니다.const tokens = [...acorn.tokenizer(code, options)]
토큰
오래 검색했는데 토큰 데이터 구조에 대한 자세한 소개가 없어서 직접 살펴보게 되었습니다.
파싱을 테스트하는 데 사용한 코드는 다음과 같습니다.
파싱된 토큰 배열은 다음과 유사한 개체입니다.실제로는 매우 비슷해 보입니다. 이해하기 쉽죠? type에 해당하는 객체에서 label은 현재 식별되는 유형을 나타내고,keyword는 예시에서 import와 같은 키워드 또는 함수입니다.
import "hello.js" var a = 2; // test function name() { console.log(arguments); }
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 },
에스트리 스펙
这一部分是重头戏,因为实际上我需要的还是解析出来的 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 这个东西对于前端来说,我们无时无刻不在享受着它带来的成果(模块构建,代码压缩,代码混淆),所以了解一下总归有好处。