本文的目的,不是针对现有的可用于生产环境的JSON、XML解析器源码进行剖析,而是介绍文本扫描的基础方法
next(char)
,并以此为核心武器,根据目标语言的词法和语法特色,一步步地组织出条例清晰、易维护的解析器代码。但愿这会是一篇实践性强,让您有所收获的文章。javascript另外,这里须要提早说明的是,本文所实现的解析器仅做为coding练习使用。一些目标语言的规范中提到的语法,可能没法正常解析。另外,本文所实现的解析器也缺乏大量的实例进行测试。请不要用于生产用途。前端
做为一个非科班前端程序员,我最近特别痴迷于自学《编译原理》这门课。缘由在于,本身大学时代的专业是语言学,其中的理论有颇多类似之处;再加上前端工做中,模版编译成render function,webpack经过loader加载文件等都方面涉及到了编译。我也但愿本身能多了解一些编译知识,说不定能在往后的前端工做中可以发挥奇效。java
看了一些youtube上的公开课资源,啃了一点龙书这样的编译原理经典做品后,我感受本身只了解了一堆关于词法法解析、语法解析的理论总结,很难从中得到“学会了”、“会用了”这样的成就感。因而在稍稍有了一点知识基础后,我开始寻找github上关于解析器的源码。node
这里想给你们推荐的是JSON之父,Douglas Crockford的repo: JSON-js中的这个源代码,它也是本文的灵感源泉:webpack
JSON-js/json_parse.js at master · douglascrockford/JSON-jsgit
据代码注释,这个文件实现了JSON.parse方法,使用的解析手段是recursive decending(递归降低分析)。程序员
在同一个repo里还有一个
json_parse_state.js
文件,也是JSON.parse方法的实现,使用的解析手段是state machine(状态机)。github其实我我的认为上文连接中的源代码使用的解析手段也是state machine,由于recursive decending应该是语法分析使用的方法来着= =。web
但从代码的清晰度上来看,json_parse.js要好很多,因此更推荐阅读。json
快速地过一遍源码,咱们能够发现一个核心函数:
var next = function (c) {
// If a c parameter is provided, verify that it matches the current character.
if (c && c !== ch) {
error("Expected '" + c + "' instead of '" + ch + "'");
}
// Get the next character. When there are no more characters,
// return the empty string.
ch = text.charAt(at);
at += 1;
return ch;
};
复制代码
这个方法至关于一个字符扫描器,其中使用的全局变量at
是当前扫描光标所处位置的索引,ch
是当前扫描光标所处位置的字符。调用next方法时,若是传入了参数c
(也是一个字符),则会比较此字符与当前扫描器所在的字符,若是不相同就会报错,而且扫描光标不会向前移动;若是未传参数,扫描光标的位置和所指的字符都会向前更新一个位置。
这份代码中的其余函数,充斥着对next的调用,让咱们来看几个例子感觉一下next的用法。
var word = function () {
// true, false, or null.
switch (ch) {
case "t":
next("t");
next("r");
next("u");
next("e");
return true;
case "f":
next("f");
next("a");
next("l");
next("s");
next("e");
return false;
case "n":
next("n");
next("u");
next("l");
next("l");
return null;
}
error("Unexpected '" + ch + "'");
};
复制代码
word函数用来处理JSON中的三个常量token,即true
, false
和null
。整个函数根据首字母,分别接收t->r->u->e,f->a->l->s->e,n->u->l->l这样的字符输入。若是其中出现了其余顺序的字符输入,都会抛出Error。word方法还会在匹配token的同时,返回所匹配到的token的值。3个return语句所出现的位置,表示word函数已经接受了这段字符输入,并成功解析出了一个值。
再来看另外一个不传参调用next()的例子:
var white = function () {
// Skip whitespace.
while (ch && ch <= " ") {
next();
}
};
复制代码
white函数的做用仅在于跳过空白,只要当前字符是属于空白的,就不停地调用next()做无条件后跳。
源码中还有number和string函数,其用途和上面的word, white同样,只不过逻辑更为复杂,能够解析出不定长度、不定字符组合的数字和字符串。
一步一步地写出这些“零件”的解析函数后,咱们就能够进一步写出一些复合结构的解析函数了,也就是源码中的array和object函数。
最后,源码中实现了能够解析任意一个JSON元素的value函数。从语法的角度讲,这里所定义的value,能够是任何一个string, number, array或object,至此,咱们就完成了解析全部JSON元素须要的函数。
以上就是解析的核心代码了,我的认为十分地易于理解而且有明确的分层,易于维护以及之后增长功能。我也在这里用一样的next函数的手法,尝试重写了这个JSON解析器源代码。做为练习,我没有实现escape或revive等功能,但把各个解析函数拆分得更加精细(例如为每一个单字符token都写了解析函数,将array拆解为[ + elements + ]
等),使得代码更易读。地址是:
有了上面的JSON解析器实现的“手感”,我又尝试着用一样的next函数手法,部分地实现了XML的解析。和JSON相比,我的在实现过程当中发现的坑点主要在于:
Node {
tagName //节点标签名
attrs //节点上的属性,为key/value的数组
children //节点的子节点,为Node的数组
}
复制代码
<a><b></a></b>
这样的XML须要提示解析错误。不过实现这个也很简单,使用一个nodeStack栈,在opentag时推入节点;在closetag时检查当前节点是否和栈尾的tag相匹配,匹配则推出末尾的节点;在comment节点或text节点时不做处理便可。<!--content-->
,所以在解析content部分时,每输入一个字符,须要做3个字符的提早判断。即,若是当前所读到的字符的接下来三个字符分别是-->
时,中止解析。我所实现的XML解析器的代码以下(没有实现self-closing tag的解析功能,例如<br>
, <input>
等。全部tag必须成对出现):