javascript语法树
更新日期:
这个月公司业务比较多,累死了,好久不写博客了。罪过罪过。花了段时间研究了下javascript的语法树,记录一下。虽然跟平时的写代码没有太多关系,但是了解下js的语法树结构还是有必要的。
javascript解析流程
在我还在上大学的时候,那时候参加软考,依稀的学过,作为一门语言。执行的时候大致需要经过以下过程:
词法分析=>语法分析=>语义分析=>中间代码生成=>优化代码=>代码生成
当然这是编译型的语言的一般步骤。
但是对于javascript这样的解释性语言,其实只有 前面的词法分析还有语法分析,词法分析就是挨个字符的去扫描源代码,把关键token识别出来。之后通过语法分析,建立上下文关系语法树ast(abstract syntax tree),解释器再根据语法树开始解释执行。所以会比c,c++这类的慢很多。
java是生成了一个jvm的中间代码。而php也是可以生成opcode来加快速度。当然其实最新的javascript解析引擎,比如V8也做了优化,会将部分js代码编译成目标代码。
语法树详解
关于语法树里面的各个节点的规范在这里可以看到:SpiderMonkey
SpiderMonkey是Mozilla项目的一部分,是一个用C语言实现的JavaScript脚本引擎
首先我们看一个简单的例子,有很多库还有工具可以解析出js的语法树。比如esprima,acorn。
esprima有个很赞的在线parse的web页面:http://esprima.org/demo/parse.html#
我们直接看个最简单的列子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | var a = 1,b=2; /*解析后的语法树 { "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": 1, "raw": "1" } }, { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "b" }, "init": { "type": "Literal", "value": 2, "raw": "2" } } ], "kind": "var" } ] } */ |
json格式的语法树,每个对象就是一个节点(Node)。可以看到每个节点都有一个type属性用来标识当前节点的类型。
Program类型,是语法树的根节点,只有一个。它具有body属性包括所有的语法节点。然后整个的var a = 1,b=2;
是一个变量声明语句(VariableDeclaration)。这个语句里面有一些变量声明符(VariableDeclarator),这边就是a=1
和b=2
;变量声明符下面有两个属性,分别是id和init。也就是对应的左边的被赋值者和右边的值。Identifier与Literal都可以表示最小的单位,他们不再包含其他节点。
细细查看就会发现其实语法树并不复杂。也是一个包含的关系。下面按照节点的类型把各种节点都解释下:
语句(Statement)
什么是语句呢,粗俗点,可以加分号的,就代表一个语句结束了。
语句也分很多种语句:ExpressionStatement,IfStatement,BreakStatement等等等,详细的可以去规范里面看。
这种类型的后面都会跟上Statement。
看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 | /*ExpressionStatement*/ a = b; /*ExpressionStatement*/ /*IfStatement*/ if(a==b){ .... } /*IfStatement*/ |
还是可以很容易看出来的。这其中有个特殊的语句,BlockStatement。说白了就是两个{}括号之间的内容称为一个BlockStatement。所以像if语句,switch语句等等都会有这样的一个子节点。因为他们都会跟花括号嘛。
具体的每个语句有哪些属性节点代表什么都可以在规范里面找到一目了然。
表达式(Expressions)
语句下面一般会包含表达式还有一些其他的杂项节点(后面会说)。比如赋值表达式(AssignmentExpression)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | a = b /* { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "AssignmentExpression", "operator": "=", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } } } ] } */ |
可以看到表达式语句(ExpressionStatement)下面有一个赋值表达式(AssignmentExpression)。可以说表达式是语句的重要组成部件。
表达式也有很多中,比如ArrayExpression,FunctionExpression等等。具体每个表达式有哪些属性,每个属性可以是哪些类型的子节点,还是建议看下规范。
声明(Declaration)
声明其实也可以不单独拿出来的,其实声明就可以当成语句。作为一种特殊的语句。
声明包括两个FunctionDeclaration,VariableDeclaration。
1 2 3 4 5 6 7 | //FunctionDeclaration function aa(){ } //VariableDeclaration var a = 1; //let const 也算,当然这是es6才支持的 |
然后这边要特别提到VariableDeclarator,它是VariableDeclaration下面附属的一个子节点,可以当作特殊的表达式吧。
子句(Clauses)
字句包括SwitchCase,CatchClause分别指 try后面跟的 catch语句,还有switch后面的case语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | switch(a){ /*SwitchCase*/ case 1: var a = b; /*SwitchCase*/ } try{ } /*CatchClause*/ catch(e) { } /*CatchClause*/ |
模式(Patterns)
这个其实是es6的内容了。es6支持一种非常酷炫的解构操作(Destructuring)。
可以参考这篇博客:http://odetocode.com/blogs/scott/archive/2014/09/11/features-of-es6-part-6-destructuring.aspx
或者ruanyifeng的书:http://es6.ruanyifeng.com/#docs/destructuring
简单的说就是可以这么做了:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var [a,b]=[1,2]; /* a 1 b 2 */ var {a,b}={a:1,b:2} /* a 1 b 2 */ |
其中 [a,b]和{a,b}就分别代表ArrayPattern 和ObjectPattern。
这个例子是es6的,文章开头给的在线parse的地址不支持的,可以自己用acorn指定版本号跑个代码看看。
解析语法树就是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | var [a,b]=[1,2]; /* { "type": "Program", "body": [{ "type": "VariableDeclaration", "declarations": [{ "id": { "type": "ArrayPattern",//ArrayPattern "elements": [{ "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" }] }, "init": { "type": "ArrayExpression", 。。。 } }], "kind": "var" }] } */ var {a,b}={a:1,b:2} /* { "type": "Program", "body": [{ "type": "VariableDeclaration", "declarations": [{ "type": "VariableDeclarator", "id": { "type": "ObjectPattern", //ObjectPattern "properties": [{ "type": "Property", "method": false, "shorthand": true, "computed": false, "key": { "type": "Identifier", "name": "a" }, "kind": "init", "value": { "type": "Identifier", "name": "a" } }, { "type": "Property", "method": false, "shorthand": true, "computed": false, "key": { "type": "Identifier", "name": "b" }, "kind": "init", "value": { "type": "Identifier", "name": "b" } }] }, "init": { "type": "ObjectExpression", ... } }], "kind": "var" }] } */ |
杂项(Miscellaneous)
杂项就是不在上面那些范围的东西。比如我们总是看见的最小组成元素Identifier,Literal。
Identifier与Literal都可以表示最小的单位,他们不再包含其他节点。
区别是Identifier是标识符一般我们使用的变量都是标识符。而Literal是文字,可以是正则表达式,字符串(也就是带引号的),null,数字等等。
看下面的对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var a = { "aaa":b, test:null } /* b是Identifier test是Identifier “aaa”是Literal null是Literal */ |
然后还有一堆的操作符:这些操作符都不能成为一个节点,而是直接作为一个值在语法树里面:
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | a = b /* { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "AssignmentExpression", "operator": "=",//直接作为一个值,而不是子节点 "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } } } ] } */ |
有下面这些操作符:
一元运算符:”-“ | “+” | “!” | “~” | “typeof” | “void” | “delete”
二元操作符: “==” | “!=” | “===” | “!==”
| “<” | “<=” | “>” | “>=”
| “<<” | “>>” | “>>>”
| “+” | “-“ | “*” | “/“ | “%”
| “|” | “^” | “in”
| “instanceof” | “..”
逻辑操作符:”||” | “&&”
赋值操作符:”=” | “+=” | “-=” | “*=” | “/=” | “%=”
| “<<=” | “>>=” | “>>>=”
| “|=” | “^=” | “&=”
自增自减操作符:”++” | “—“
最后还有些特殊的节点,比如ObjectExpression的property,VariableDeclaration的VariableDeclarator。我觉得都是可以归到这边的杂项的。
结语
语法树的结构还是内容比较多的。不过只要了解节点的大概结构。还是可以很容易看懂语法树的。可以使用上面的在线解析语法树的工具没事试些简单的多对比对比,就可以很清楚了。
有人说知道语法树有什么用呢,其实还是很有用的。比如你要做代码压缩,语法高亮,关键字匹配,作用域判断。等等等都是最好先把代码解析成语法树之后再去各种操作的。我是因为有个需求需要修改js代码的结构,所以也是需要先把代码解析成语法树。当然仅仅解析还不够,还要提供api去遍历修改语法树。最好直接就是类似jQuery那样的api。这个就是另外一件事了,以后有空也记录下。