当前位置:必发彩票官网 > 词法分析 >

使用Acorn来解析JavaScript

  因为最近工作上有需要使用解析 JavaScript 的代码,大部分情况使用正则表达式匹配就可以处理,但是一旦依赖于代码上下文的内容时,正则或者简单的字符解析就很力不从心了,这个时候需要一个语言解析器来获取整一个 AST(abstract syntax tree)。

  从提交记录来看,维护情况都蛮好的,ES 各种发展的特性都跟得上,我分别都简单了解了一下,聊聊他们的一些情况。

  Esprima 是很经典的一个解析器,Acorn 在它之后诞生,都是几年前的事情了。按照 Acorn 作者的说法,当时造这个轮子更多只是好玩,速度可以和 Esprima 媲美,但是实现代码更少。其中比较关键的点是这两个解析器出来的 AST 结果(对,只是 AST,tokens 不一样)都是符合 The Estree Spec 规范(这是 Mozilla 的工程师给出的 SpiderMonkey 引擎输出的 JavaScript AST 的规范文档,也可以参考:SpiderMonkey in MDN)的,也就是得到的结果在很大部分上是兼容的。

  至于 Uglify,很出名的一个 JavaScript 代码压缩器,其实它自带了一个代码解析器,也可以输出 AST,但是它的功能更多还是用于压缩代码,如果拿来解析代码感觉不够纯粹。

  Esprima 官网上有一个性能测试,我在 chrome 上跑的结果如下:

  可见,Acorn 的性能很不错,而且还有一个 Estree 的规范呢(规范很重要,我个人觉得遵循通用的规范是代码复用的重要基础),所以我就直接选用 Acorn 来做代码解析了。

  Acorn 的配置项蛮多的,里边还包括了一些事件可以设置回调函数。我们挑几个比较重要的讲下:

  字面意义,很好理解,就是设置你要解析的 JavaScript 的 ECMA 版本。默认是 ES7。

  所以,选择了 script 则出现 import/export 会报错,可以使用严格模式声明,选择了 module,则不用严格模式声明,可以使用import/export 语法。

  默认值是 false,设置为 true 之后会在 AST 的节点中携带多一个 loc 对象来表示当前的开始和结束的行数和列数。

  传入一个回调函数,每当解析到代码中的注释时会触发,可以获取当年注释内容,参数列表是:[block, text, start, end]。

  block 表示是否是块注释,text 是注释内容,start 和 end 是注释开始和结束的位置。

  词法结果 token 和 Esprima 的结果数据结构上有一定的区别(Espree 又是做了这一层的兼容),有兴趣了解的可以看下 Esprima 的解析结果:。

  我找了半天,没找到关于 token 数据结构的详细介绍,只能自己动手来看一下了。

  看上去其实很好理解对不对,在 type 对应的对象中,label 表示当前标识的一个类型,keyword 就是关键词,像例子中的import,或者 function 之类的。

  value 则是当前标识的值,start/end 分别是开始和结束的位置。

  这一部分是重头戏,因为实际上我需要的还是解析出来的 AST。最原滋原味的内容来自于:The Estree Spec,我只是阅读了之后的搬运工。

  提供了标准文档的好处是,很多东西有迹可循,这里还有一个工具,用于把满足 Estree 标准的 AST 转换为 ESMAScript 代码:escodegen。

  好吧,回到正题,我们先来看一下 ES5 的部分,可以在 Esprima: Parser 这个页面测试各种代码的解析结果。

  符合这个规范的解析出来的 AST 节点用 Node 对象来标识,Node 对象应该符合这样的接口:

  type 字段表示不同的节点类型,下边会再讲一下各个类型的情况,分别对应了 JavaScript 中的什么语法。

  loc 字段表示源码的位置信息,如果没有相关信息的话为 null,否则是一个对象,包含了开始和结束的位置。接口如下:

  这里的 Position 对象包含了行和列的信息,行从 1 开始,列从 0 开始:

  好了,基础部分就是这样,接下来看各种类型的节点,顺带温习一下 JavaScript 语法的一些东西吧。对于这里每一部分的内容,会简单谈一下,但不会展开(内容不少),对 JavaScript 了解的人很容易就明白的。

  标识符,我觉得应该是这么叫的,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符。相应的接口是这样的:

  一个标识符可能是一个表达式,或者是解构的模式(ES6 中的解构语法)。我们等会会看到 Expression 和 Pattern 相关的内容的。

  字面量,这里不是指 [] 或者 {} 这些,而是本身语义就代表了一个值的字面量,如 1,“hello”, true 这些,还有正则表达式(有一个扩展的 Node 来表示正则表达式),如 /d?/。我们看一下文档的定义:

  value 这里即对应了字面量的值,我们可以看出字面量值的类型,字符串,布尔,数值,null 和正则。

  这个针对正则字面量的,为了更好地来解析正则表达式的内容,添加多一个 regex 字段,里边会包括正则本身,以及正则的flags。

  body 属性是一个数组,包含了多个 Statement(即语句)节点。

  id 是函数名,params 属性是一个数组,表示函数的参数。body 是一个块语句。

  这让人感觉这个文档规划得蛮细致的,函数名,参数和函数块是属于函数部分的内容,而声明或者表达式则有它自己需要的东西。

  语句节点没什么特别的,它只是一个节点,一种区分,但是语句有很多种,下边会详述。

  表达式语句节点,a = a + 1 或者 a++ 里边会有一个 expression 属性指向一个表达式节点对象(后边会提及表达式)。

  块语句节点,举个例子:if (...) { // 这里是块语句的内容 },块里边可以包含多个其他的语句,所以有一个 body 属性,是一个数组,表示了块里边的多个语句。

  with 语句节点,里边有两个特别的属性,object 表示 with 要使用的那个对象(可以是一个表达式),body 则是对应 with 后边要执行的语句,一般会是一个块语句。

  这里的 loop 就是一个 label 了,我们可以在循环嵌套中使用 break loop 来指定跳出哪个循环。所以这里的 label 语句指的就是loop: ... 这个。

  一个 label 语句节点会有两个属性,一个 label 属性表示 label 的名称,另外一个 body 属性指向对应的语句,通常是一个循环语句或者 switch 语句。

  break 语句节点,会有一个 label 属性表示需要的 label 名称,当不需要 label 的时候(通常都不需要),便是 null。

  if 语句节点,很常见,会带有三个属性,test 属性表示 if (...) 括号中的表达式。

  consequent 属性是表示条件为 true 时的执行语句,通常会是一个块语句。

  alternate 属性则是用来表示 else 后跟随的语句节点,通常也会是块语句,但也可以又是一个 if 语句节点,即类似这样的结构:

  switch 语句节点,有两个属性,discriminant 属性表示 switch 语句后紧随的表达式,通常会是一个变量,cases 属性是一个case 节点的数组,用来表示各个 case 语句。

  try 语句节点,block 属性表示 try 的执行语句,通常是一个块语句。

  while 语句节点,test 表示括号中的表达式,body 是表示要循环执行的语句。

  for 循环语句节点,属性 init/test/update 分别表示了 for 语句括号中的三个表达式,初始化值,循环判断条件,每次循环执行的变量更新语句(init 可以是变量声明或者表达式)。这三个属性都可以为 null,即 for(;;){}。

  for/in 语句节点,left 和 right 属性分别表示在 in 关键词左右的语句(左侧可以是一个变量声明或者表达式)。body 依旧是表示要循环执行的语句。

  声明语句节点,同样也是语句,只是一个类型的细化。下边会介绍各种声明语句类型。

  函数声明,和之前提到的 Function 不同的是,id 不能为 null。

  变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。

  变量声明的描述,id 表示变量名称节点,init 表示初始值的表达式,可以为 null。

  数组表达式节点,elements 属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。

  对象表达式节点,property 属性是一个数组,表示对象的每一个键值对,每一个元素都是一个属性节点。

  对象表达式中的属性节点。key 表示键,value 表示值,由于 ES5 语法中有 get/set 的存在,所以有一个 kind 属性,用来表示是普通的初始化,或者是 get/set。

  一元运算表达式节点(++/-- 是 update 运算符,不在这个范畴内),operator 表示运算符,prefix 表示是否为前缀运算符。argument 是要执行运算的表达式。

  update 运算表达式节点,即 ++/--,和一元运算符类似,只是 operator 指向的节点对象类型不同,这里是 update 运算符。

  二元运算表达式节点,left 和 right 表示运算符左右的两个表达式,operator 表示一个二元运算符。

  赋值表达式节点,operator 属性表示一个赋值运算符,left 和 right 是赋值运算符左右的表达式。

  逻辑运算表达式节点,和赋值或者二元运算类型,只不过 operator 是逻辑运算符类型。

  条件表达式,通常我们称之为三元运算表达式,即 boolean ? true : false。属性参考条件语句。

  函数调用表达式,即表示了 func(1, 2) 这一类型的语句。callee 属性是一个表达式节点,表示函数,arguments 是一个数组,元素是表达式节点,表示函数参数列表。

  这个就是逗号运算符构建的表达式(不知道确切的名称),expressions 属性为一个数组,即表示构成整个表达式,被逗号分割的多个表达式。

  模式,主要在 ES6 的解构赋值中有意义,在 ES5 中,可以理解为和 Identifier 差不多的东西。

  这一部分的内容比较多,但都可以举一反三,写这个的时候我就当把 JavaScript 语法再复习一遍。这个文档还有 ES2015,ES2016,ES2017 相关的内容,涉及的东西也蛮多,但是理解了上边的这一些,然后从语法层面去思考这个文档,其他的内容也就很好理解了,这里略去,有需要请参阅:The Estree Spec。

  回到我们的主角,Acorn,提供了一种扩展的方式来编写相关的插件:Acorn Plugins。

  我们可以使用插件来扩展解析器,来解析更多的一些语法,如 .jsx 语法,有兴趣的看看这个插件:acorn-jsx。

  官方表示 Acorn 的插件是用于方便扩展解析器,但是需要对 Acorn 内部的运行极致比较了解,扩展的方式会在原本的基础上重新定义一些方法。这里不展开讲了,如果我需要插件的话,会再写文章聊聊这个东西。

  现在我们来看一下如何应用这个解析器,例如我们需要用来解析出一个符合 CommonJS 规范的模块依赖了哪些模块,我们可以用 Acorn 来解析 require 这个函数的调用,然后取出调用时的传入参数,便可以获取依赖的模块。

  这只是简单的一个情况的处理,但是已经给我们呈现了如何使用解析器,Webpack 则在这个的基础上做了更多的东西,包括 var r = require; r(a) 或者 require.async(a) 等的处理。

  AST 这个东西对于前端来说,我们无时无刻不在享受着它带来的成果(模块构建,代码压缩,代码混淆),所以了解一下总归有好处。

  每日头条、业界资讯、热点资讯、八卦爆料,全天跟踪微博播报。各种爆料、内幕、花边、资讯一网打尽。百万互联网粉丝互动参与,TechWeb官方微博期待您的关注。

http://syn992.com/cifafenxi/132.html
点击次数:??更新时间2019-04-12??【打印此页】??【关闭
  • Copyright © 2002-2017 DEDECMS. 织梦科技 版权所有  
  • 点击这里给我发消息
在线交流 
客服咨询
【我们的专业】
【效果的保证】
【百度百科】
【因为有我】
【所以精彩】