Hello World
Spiga

UglifyJS有个不错的JavaScript解析器

2011-04-01 15:40 by 老赵, 7976 visits

我一直在为Jscex寻找好用的JavaScript解析器,之前我用的是Narcissus,也写过相关文章。不过可惜的是,Narcissus使用了SpiderMonkey的扩展,因此它并不是用ECMAScript 3实现的,无法在IE 8等浏览器中使用。目前Jscex使用的是NarrativeJS中旧版的Narcissus,但是我并不喜欢它输出的AST结构,使用中也发现高级功能里的一些bug,有些食之无味弃之可惜的感觉,而改写新版Narcissus又必须大动干戈。最近我接触到了UglifyJS,发现它的解析器相当不错,性能也比Narcissus高出许多,在此介绍给大家。

介绍

UglifyJS是个JavaScript压缩器,效果和Google Closure Compiler相比有过之而无不及。对于现代化的JavaScript压缩器来说,简单的去除空白和压缩局部变量是远远不够的,同时需要理解代码的语义,将其替换成提及更小的形式(Uglify的说明页上有许多描述)。这显然需要一个JavaScript解析器。UglifyJS基于NodeJS开发,不过可以在各种支持CommonJS模块系统的JavaScript引擎/平台上运行。如果没有CommonJS,也只需将exports相关的代码去掉即可。

JavaScript解析器的作用自然是将JavaScript代码分解成AST,然后根据AST便可以做到许多有趣的事情。相同的AST可以在内存中有不同的表现形式,例如之前提到我不太喜欢Jscex目前使用的旧版Narcissus,一个重要的原因便是它的AST结构不够友好(最新的Narcissus倒不错)。此外,虽然它提供了一些高级功能,例如标注了每个元素在源代码中的位置,这样使用者就可以直接根据getSource方法获得它对应的源代码——只可惜经试验这个功能有bug,这迫使我还得遍历完整的AST。

UglifyJS的JavaScript分词器和解析器存放在源代码的parse-js.js文件中,移植于parse-js项目,后者是一个用Common Lisp实现的类库。现在您应该可以猜到它输出的AST是什么表现形式了吧。没错,就是个“表”,用JavaScript来表示,就是个数组套数组。我写了点简单的代码对其进行格式化输出,您可以在这里简单尝试一下UglifyJS的解析器。这个输出虽然简单,但对于Jscex来说也已经完全够用了。

使用

打开parse-js.js文件,您会看到这样一些代码:

/* -----[ Tokenizer (constants) ]----- */

var KEYWORDS = array_to_hash([
    ...
]);

var RESERVED_WORDS = array_to_hash([
    ...
]);

...

function parse($TEXT, exigent_mode, embed_tokens) {
    ...
}

...

/* -----[ Exports ]----- */

exports.tokenizer = tokenizer;
exports.parse = parse;
exports.slice = slice;
exports.curry = curry;
exports.member = member;
exports.array_to_hash = array_to_hash;
exports.PRECEDENCE = PRECEDENCE;
exports.KEYWORDS_ATOM = KEYWORDS_ATOM;
exports.RESERVED_WORDS = RESERVED_WORDS;
exports.KEYWORDS = KEYWORDS;
exports.ATOMIC_START_TOKEN = ATOMIC_START_TOKEN;
exports.OPERATORS = OPERATORS;
exports.is_alphanumeric_char = is_alphanumeric_char;
exports.set_logger = function(logger) {
        warn = logger;
};

UglifyJS是基于CommonJS模块机制编写的,这一个文件其实就是个模块,它对外的方法通过exports暴露出来。如果我们将其作为普通的JavaScript文件引入到浏览器中,显然会报“export未定义”异常。理论上说,如果定义一个exports对象,甚至去除和exports有关的代码就能正常使用parse方法了。不过这么做也有个严重的问题,那就是对根对象的“污染”实在是太严重了,例如在浏览器中所有的函数,变量都出现在window上,再引入一些其他类库,造成冲突的可能性相当高。

因此,我们必须对代码进行一些修改。幸运的是,在JavaScript中解决这类“作用域”问题十分容易,例如我这样将parse-js.js的代码包围了起来:

var UglifyJS = {};

(function (exports) {

/* original code here */

})(UglifyJS);

这样就解决了作用域问题,如今我们就能访问UglifyJS对象上的KEYWORDS集合以及parse等成员了。

性能

然后再说说性能。JavaScript一直被认为是一门执行效率低下的语言——这其实是个错误的观点。其实从语言设计上说,JavaScript比Python和Ruby都要快,只不过由于历史原因各大浏览器对它都不太重视而已。不过如今情况早就有所改变,在V8的带领下,现代的JavaScript引擎执行速度都已经超过了目前最快的Python和Ruby实现。话不多说,现在我们就来比较一下UglifyJS的解析器与Narcissus在各浏览器下的表现吧。

测试页面在此,您也可以自行尝试,测试场景是使用两者分别解析十次Narcissus的源代码——大约1500行未压缩的JavaScript代码(值得一提的是,我试了许多压缩后的代码,如jquery-min.js,它们用UgilifyJS可以正常解析,而Narcissus却解析失败)。我使用两台公司配置的标准工作机,测试了IE、Chrome和Firefox各两个版本共6种浏览器。每个浏览器我都会运行多遍测试,去除偏差大的结果,取中游数值。遗憾的是,由于条件所限,两台机器的操作系统有所不同,虽然我认为并不会对结果有什么影响,但如果您足够顶真,也不妨再自行评测一把。

首先我在Win 7下测试了Chrome 10、FireFox 3和IE9,结果如下:

对于UglifyJS来说,Chrome 10的表现最好,IE 9相比略慢少许,而Firefox 3耗时则是前两者的数倍。对于Narcissus来说,则是IE 9表现最好,仅为Chrome 10的五分之一,和Firefox 3相比更是数量级上的领先。有趣的是,Chrome 10和Firefox 3下两个解析器的耗时都是一比十左右,而IE 9下则相差无几。

然后是Win XP下Chromium 12、Firefox 4及IE 8,结果如下:

对于UglifyJS来说,Chromium 12的表现依旧抢眼,胜过Firefox 4不少,不过使用Narcissus的情况则正好相反。同样可以看出,IE 8的JavaScript引擎性能全面不敌其他浏览器,不过它和IE 9、Firefox 4(以及后面的Safari)的情况类似,即UglifyJS和Narcissus的耗时并没有太大差别。

为了便于观察,我将两次测试的结果放在一起(除了非正式版本的Chromium 12):

总体而言,Chrome 10、IE 9和Firefox 4为第一军团。IE 9在UglifyJS上小负于Chrome 10,但在Narcissus上优势明显;Chrome 10在UglifyJS上表现最佳,但在Narcissus却落后IE 9和Firefox 4较多;Firefox 4虽然都不是“最佳”,但差距也并不太大。至于IE 8和Firefox 3,在JavaScript的执行效率方面的确已经落后于这个时代了。必须承认,如今的浏览器大战的确大大提高了各方的质量。

此外我还测试了公司iMac上的Chrome 10、Firefox 3以及Safari 5,在此列出结果:

虽然浏览器的表现各有高低,差距也有所不同,但可以确定的是,UglifyJS解析器的性能的确比Narcissus要高。因此,我打算在接下来几天里用UglifyJS替换掉目前Jscex里使用的Narcissus。

总结

由于前端开发和JavaScirpt的流行,越来越多的人开始用JavaScript做一些有趣的事情。我很不喜欢如今许多所谓的前端实践,纠缠于大量的hack以及各种浏览器的表现,甚至是JavaScript里某种特定写法的性能更高——例如,居然有消息称,对于字符串连接操作来说,a += b的性能比a = a + b要高(或反之)。在我看来这些东西是最无用的,知道了又如何?随着浏览器更新换代,这些“经验”瞬间就毫无作用了。

这也是我为什么喜欢玩JavaScript,却死也不愿去做前端开发,尤其是HTML、CSS。同样,如IE 6这种浏览器在我眼中也是必须消灭的东西。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

22 条回复

  1. Belleve Invis
    218.22.21.*
    链接

    Belleve Invis 2011-04-01 17:02:41

    占沙发~

    AST 有没有注释节点?我想用来做高亮 + 格式化w

  2. 老赵
    admin
    链接

    老赵 2011-04-01 17:04:50

    @Belleve Invis

    自己去文章里的那个页面跑跑看?

  3. Belleve Invis
    218.22.21.*
    链接

    Belleve Invis 2011-04-01 17:06:41

    @老赵

    没有 555555…… 算了我还是用 Narcissus 好了。

  4. amen
    114.251.186.*
    链接

    amen 2011-04-02 09:51:28

    @老赵

    你博客首页上的照片上应该只伸出一根指头更有喜感…… 不是吗?

    http://www.wangxiaofeng.net/?p=7285

  5. Sean
    180.168.12.*
    链接

    Sean 2011-04-02 10:43:05

    你那几个结果页面全被墙咯。悲剧~

  6. 老赵
    admin
    链接

    老赵 2011-04-02 10:50:23

    @Sean

    精华都在里面,想办法看看吧,哈哈。

  7. Kenny Yuan
    134.134.137.*
    链接

    Kenny Yuan 2011-04-02 11:15:36

    广义表不就是树么?呵呵

  8. 老赵
    admin
    链接

    老赵 2011-04-02 11:36:53

    @Kenny Yuan

    本来就是“AST的表现形式”嘛。

  9. 则名
    121.18.127.*
    链接

    则名 2011-04-02 15:26:57

    最后一句话真实道出了web前端开发从业者的境遇。

  10. Belleve Invis
    218.22.21.*
    链接

    Belleve Invis 2011-04-02 15:35:21

    前端是坑,无敌大坑…… 这也是我为何要做 Eisa。

    ps. 现在已经突破天际了……(大雾)

    另外给 jscex 说声:$pattern 支持的语法环境太少啦!我想要连这样的也支持:

    f(1, 2, 3, $pattern(get(...)), 4, 5, 6)
    
  11. 老赵
    admin
    链接

    老赵 2011-04-02 16:24:23

    @Belleve Invis

    我就先做到F#目前的样子了,其实已经比它复杂了,因为我还支持break和continue什么什么的。你说的这个不是不能,但其实很多时候会麻烦许多,例如:

    f(g(1), $await(...))
    

    按理说是g(1)先执行,然后在$await,那么生成后的代码复杂度就一下子提高了。而且其实理想情况下还有更多复杂情况,比如:

    if (x > y && $await(...)) { ... }
    

    $await甚至可能被短路掉。要做好不是做不到,但是太复杂了,在可预见的将来估计不会打算去支持。

    所以,就把这种顺序问题交给用户自己去控制吧,比如:

    var a1 = g(1);
    var a2 = $await(...);
    f(a1, a2);
    

    以及

    if (x > y) {
        var flag = $await(...);
        if (flag) { ... }
    }
    

    总之,保持规则简单,源代码和目标代码映射关系清晰。

  12. shirley
    120.193.153.*
    链接

    shirley 2011-04-02 21:32:51

    你好,很幸运地误打误入了你的博客,我看到你的个人陈述上说反对恶劣的培训机构误导懵懂时期的初学者,这也是我现在所纠结的问题,我是一名在校生,所以想问问你有没有什么培训机构可以推荐一下,或者有什么培训机构很不好建议不去的?

    期待你的回答。

  13. 你为什么不更新博客园的博客了?
    112.94.185.*
    链接

    你为什么不更新博客园的博客了? 2011-04-04 00:59:08

    你为什么不更新博客园的博客了?

  14. 老赵
    admin
    链接

    老赵 2011-04-04 16:07:36

    @你为什么不更新博客园的博客了?

    写独立博客了自然不会更新两个地方么。

  15. Belleve Invis
    60.166.104.*
    链接

    Belleve Invis 2011-04-05 21:36:25

    @老赵

    那你能不能在 $pattern 用在其他地方的时候报异常而不是保持不变?

    我就先做到F#目前的样子了,其实已经比它复杂了,因为我还支持break和continue什么什么的。你说的这个不是不能,但其实很多时候会麻烦许多,例如……

  16. Teddy
    116.237.220.*
    链接

    Teddy 2011-04-05 23:11:18

    所有的iframe里的内容都无法访问“SSL connection error”,是不是整个截图好一些?

  17. 老赵
    admin
    链接

    老赵 2011-04-06 07:29:06

    @Belleve Invis

    嗯嗯,这个的确是可以考虑的。

  18. 老赵
    admin
    链接

    老赵 2011-04-06 07:29:33

    @Teddy

    嘿嘿,你看到了就能知道这效果非截图可比啊。

  19. jinwyp
    69.172.214.*
    链接

    jinwyp 2011-04-07 18:02:29

    效果比截图差多了 那个图表真垃圾 我还以为能有排序或什么高级功能, 结果屁功能没有 弄个flash chart算了 要不就截图吧

  20. 老赵
    admin
    链接

    老赵 2011-04-07 21:51:29

    @jinwyp

    把鼠标移上去还能显示数字,比截图差在哪儿?哈哈,难道截下来会更好看?

  21. dyw
    61.135.152.*
    链接

    dyw 2012-02-15 15:18:09

    Introducing esprima: blazing-fast javascript parser

    根据作者自己的测试,esprima比UglifyJS的parser快. 也是纯js实现的.

  22. 老赵
    admin
    链接

    老赵 2012-02-15 17:25:56

    @dyw

    多谢,看上去API也更友好些。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我