javascript SpiderMonkey中的函数序列化如何进行


Posted in Javascript onDecember 05, 2012

在Javascript中,函数可以很容易的被序列化(字符串化),也就是得到函数的源码.但其实这个操作的内部实现(引擎实现)并不是你想象的那么简单.SpiderMonkey中一共使用过两种函数序列化的技术:一种是利用反编译器(decompiler)将函数编译后的字节码反编译成源码字符串,另一种是在将函数编译成字节码之前就把函数源码压缩并存储下来,用到的时候再解压还原.

如何进行函数序列化
在SpiderMonkey中,能将函数序列化的方法或函数有三个:Function.prototype.toString,Function.prototype.toSource,uneval.只有toString方法是标准的,也就是各引擎通用的.但是ES标准中关于Function.prototype.toString方法的规定(ES5 15.3.4.2)只有寥寥数语,也就是说,基本没有标准,引擎自己决定该如何实现.

函数序列化的作用
函数序列化最主要的作用应该是利用序列化生成的函数源码来重新定义这个函数.

function a() { 
... 
alert("a") 
... 
} a() //执行时可能会弹出"a" 
a = eval("(" + a.toString().replace('alert("a")', 'alert("b")') + ")") 
a() //执行时可能会弹出"b"

你也许会想:"我写了这么多年Javascript,怎么没有遇到这种需求".的确,如果是自己的网站,自己完全控制的js文件,不需要以这种打补丁的方式来修改函数,直接修改就可以了.但是如果源文件不是你能控制的了的话,就很有可能要这样做了.比如常用的地方有greasemonkey脚本:你可能需要禁用或修改某个网站中的某个函数.还有就是Firefox扩展:你需要修改Firefox自身的某个函数(可以说Firefox是用JS写的).举个我自己写的Firefox脚本的例子:
location == "chrome://browser/content/browser.xul" && eval("gURLBar.handleCommand=" + gURLBar.handleCommand.toString().replace(/^\s*(load.+);/gm, "/^javascript:/.test(url)||(content.location=='about:blank'||content.location=='about:newtab')?$1:gBrowser.loadOneTab(url,{postData:postData,inBackground:false, allowThirdPartyFixup: true});"))

这个代码的作用是:在地址栏上回车时,让Firefox在新标签中打开页面,而不是占用当前标签.实现方式就是用toString方法读取到gURLBar.handleCommand函数的源码,然后用正则替换后传给eval,重新定义了这个函数.

为什么不用直接定义的方式,也就是直接重写函数呢:

gURLBar.handleCommand = function(){...//将原本的函数更改了一个小地方}
不能这么做的原因是因为我们得考虑兼容性,我们应该尽可能小的更改这个函数的源码.如果这么写的话,Firefox的gURLBar.handleCommand源码一旦发生变化,这个脚本就失效了.比如Firefox3和Firefox4中都有这个函数,但函数内容差别非常大,可是如果用正则替换部分关键字的话,只要这个被替换的这个关键字没有发生变化的话,就不会出现不兼容的现象.

反编译字节码
在SpiderMonkey中,函数在被解析之后会被编译成字节码(bytecode),也就是说,内存中存储着并不是原始的函数源码.SpiderMonkey中存在一个反编译器,它的主要作用就是把函数的字节码反编译成函数源码的形式.

在Firefox16以及之前的版本中,SpiderMonkey使用的就是这种方法,如果你使用的是这些版本的Firefox的话,可以尝试下面的代码:

alert(function () { 
"字符串"; 
//注释 
return 1 + 2 + 3 
}.toString()) 
返回的字符串是 function () { 
return 6; 
}

输出和其他的浏览器完全不同:

1.没有意义的原始值字面量在编译的时候会被删除,这个例子中就是"字符串".

你也许会觉得:"貌似没什么问题,反正这些值对于函数的运行来说并没有什么意义".等等,你是不是忘了个东西,表示严格模式的字符串"use strict"怎么办呢?

在不支持严格模式的版本中,比如Firefox3.6,这个"use strict"和其他字符串没什么区别,编译的时候会被删除.在SpiderMonkey实现了严格模式之后,虽然编译的时候同样会忽略掉这个字符串"use strict",但在反编译的时候会进行判断,如果这个函数处于严格模式中,则会在函数体的第一行添加上"use strict",下面是对应的引擎源码.

static JSBool

DecompileBody(JSPrinter *jp, JSScript *script, jsbytecode *pc) 
{ 
/* Print a strict mode code directive, if needed. */ 
if (script->strictModeCode && !jp->strict) { 
if (jp->fun && (jp->fun->flags & JSFUN_EXPR_CLOSURE)) { 
/* 
* We have no syntax for strict function expressions; 
* at least give a hint. 
*/ 
js_printf(jp, "\t/* use strict */ \n"); 
} else { 
js_printf(jp, "\t\"use strict\";\n"); 
} 
jp->strict = true; 
} jsbytecode *end = script->code + script->length; 
return DecompileCode(jp, script, pc, end - pc, 0); 
}

2.注释在编译的时候也会被删除

这个貌似没太大影响,不过有些人愿意利用函数注释来实现多行字符串,这个方法在Firefox 17之前的版本中是不可用的.

function hereDoc(f) {  
return f.toString().replace(/^.+\s/,"").replace(/.+$/,""); 
} 
var string = hereDoc(function () {/* 
我 
你 
他 
*/}); 
console.log(string)




3.原始值字面量的运算会在编译时进行.

这算是一种优化方式,《高性能JavaScript》提到过:
javascript SpiderMonkey中的函数序列化如何进行
反编译的弊端
由于新技术的出现(比如严格模式)以及在修改其他相关bug的时候,反编译器这部分的实现经常需要更改,更改就有可能产生新的bug,我自己就亲身遇到过一个bug.大概是在Firefox10左右的时候,具体问题记不大清了,反正是关于反编译时小括号是否要保留的问题,大概是这样的:

>(function (a,b,c){return (a+b)+c}).toString() 
"function (a, b, c) { 
return a + b + c; 
}"

在反编译时,(a+b)中的小括号被省略了,由于加法结合律从左到右,所以这没关系.但我遇到的bug是这样的:
>(function (a,b,c){return a+(b+c)}).toString() 
"function (a, b, c) { 
return a + b + c; 
}"

这就就不行了,a+b+c不等于a+(b+c),比如在a=1,b=2,c="3"的情况下,a+b+c等于"33",而a+(b+c)等于"123".

关于反编译器,Mozilla工程师Luke Wagner指出,反编译器对他们实现一些新功能的阻碍很大,而且经常会出现一些bug:

Not to pile on, but I too have felt an immense drag from the decompiler in the last year. Testing coverage is also poor and any non-trivial change inevitably produces fuzz bugs.The sooner we remove this drag the sooner we start reaping the benefits. In particular,I think now is a much better time to remove it than after doing significant frontend/bytecode hacking for new language features.

Brendan Eich也表示,反编译器的确有很多不理想:

I have no love for the decompiler, it has been hacked over for 17 years. 存储函数源码
从Firefox17之后,SpiderMonkey改成了第二种实现方法,其他浏览器也应该是这样实现的吧.函数序列化得到的字符串完全和源码一致,包括空白符,注释等等.这样的话,大部分问题就应该没有了吧.不过,貌似我又想到个问题.还是关于严格模式的.

比如:

(function A() { 
"use strict"; 
alert("A"); 
}) + ""

当然,返回的源码中也应该有"use strict",所有浏览器都是这么实现的:
function A() { 
"use strict"; 
alert("A"); 
}

但如果是这样呢:
(function A() { 
"use strict"; 
return function B() { 
alert("B") 
} 
})() + ""

内部函数B也处于严格模式中,输出B的函数源码应不应该加上"use strict"呢.试验一下:

上面说了,Firefox17之前Firefox4之后的版本是通过判断当前函数是否处于严格模式来决定输出不输出"use strict"的,函数B继承了函数A的严格模式,所以会有"use strict".

同时函数源码是缩进严格的,因为在反编译的时候,SpiderMonkey会给反编译出的源码进行格式化,即使之前的源码完全没有缩进也没关系:

function B() { 
"use strict"; 
alert("B"); 
}

Firefox17之后的版本会不会带有"use strict"呢?因为是直接把函数源码保存下来的,而且函数B中的确没有"use strict"字样.试验结果是:会添加上"use strict",只是缩进有点问题,因为没有格式化这一步了.
function B() { 
"use strict"; alert("B") 
}

SpiderMonkey最新版的jsfun.cpp源码中有对应的注释

// 如果一个函数的某个上层函数中拥有"use strict",那么这个函数就继承了上层函数的严格模式.
// 我们也会在这个内部函数的函数体内插入"use strict".
// 这就确保了,如果这个函数的toString方法的返回值被重新求值时,
// 重新生成的函数会和原函数有着相同的语义.

而不同的是,其他浏览器都是不带"use strict"的:

function B() { 
alert("B") 
}

虽然这不会有什么太大影响,但我觉的Firefox的实现是更合理的.
Javascript 相关文章推荐
基于jQuery的消息提示插件之旅 DivAlert(三)
Apr 01 Javascript
JQuery+DIV自定义滚动条样式的具体实现
Jun 25 Javascript
IE6中链接A的href为javascript协议时不在当前页面跳转
Jun 05 Javascript
jQuery实现移动 和 渐变特效的点击事件
Feb 26 Javascript
js+html5实现可在手机上玩的拼图游戏
Jul 17 Javascript
js实现时间轴自动排列效果
Mar 09 Javascript
windows下vue.js开发环境搭建教程
Mar 20 Javascript
解决ztree搜索中多级菜单展示不全问题
Jul 05 Javascript
微信小程序左滑删除功能开发案例详解
Nov 12 Javascript
vue App.vue中的公共组件改变值触发其他组件或.vue页面监听
May 31 Javascript
vue瀑布流组件实现上拉加载更多
Mar 10 Javascript
vue+canvas实现移动端手写签名
May 21 Javascript
javascript中有趣的反柯里化深入分析
Dec 05 #Javascript
js multiple全选与取消全选实现代码
Dec 04 #Javascript
在js(jquery)中获得文本框焦点和失去焦点的方法
Dec 04 #Javascript
关于javascript中的typeof和instanceof介绍
Dec 04 #Javascript
无缝滚动改进版支持上下左右滚动(封装成函数)
Dec 04 #Javascript
js动画(animate)简单引擎代码示例
Dec 04 #Javascript
JavaScript中“+”的陷阱深刻理解
Dec 04 #Javascript
You might like
php printf输出格式使用说明
2010/12/05 PHP
php-fpm配置详解
2014/02/12 PHP
php+ajax制作无刷新留言板
2015/10/27 PHP
php记录搜索引擎爬行记录的实现代码
2018/03/02 PHP
浅析PHP7的多进程及实例源码
2019/04/14 PHP
jQuery.extend 函数的详细用法
2012/06/27 Javascript
jQuery事件 delegate()使用方法介绍
2012/10/30 Javascript
jQuery 获取URL的GET参数值的小例子
2013/04/18 Javascript
AngularJS模仿Form表单提交的实现代码
2016/12/08 Javascript
node实现简单的反向代理服务器
2017/07/26 Javascript
Vue2.0 axios前后端登陆拦截器(实例讲解)
2017/10/27 Javascript
Js中将Long转换成日期格式的实现方法
2018/06/05 Javascript
VUE搭建手机商城心得和遇到的坑
2019/02/21 Javascript
详解如何更好的使用module vuex
2019/03/27 Javascript
Nodejs监控事件循环异常示例详解
2019/09/22 NodeJs
layui监听select变化,以及设置radio选中的方法
2019/09/24 Javascript
vue 开发企业微信整合案例分析
2019/12/02 Javascript
Python抓取框架 Scrapy的架构
2016/08/12 Python
Python 多线程实例详解
2017/03/25 Python
tensorflow saver 保存和恢复指定 tensor的实例讲解
2018/07/26 Python
Puppeteer使用示例详解
2019/06/20 Python
python实时检测键盘输入函数的示例
2019/07/17 Python
Python实现投影法分割图像示例(一)
2020/01/17 Python
Python 实现图片转字符画的示例(静态图片,gif皆可)
2020/11/05 Python
python3中for循环踩过的坑记录
2020/12/14 Python
Toppik顶丰增发纤维官网:解决头发稀疏
2017/12/30 全球购物
全球在线商店:BerryLook
2019/04/14 全球购物
Kiwi.com中国:找到特价机票并发现新目的地
2019/10/27 全球购物
应征英语教师求职信
2013/11/27 职场文书
大学生毕业鉴定
2014/01/31 职场文书
室内拓展活动方案
2014/02/13 职场文书
班班通项目实施方案
2014/02/25 职场文书
2014年公路养护工作总结
2014/12/04 职场文书
公务员年度考核登记表个人总结
2015/02/12 职场文书
5种方法告诉你如何使JavaScript 代码库更干净
2021/09/15 Javascript
redis 解决库存并发问题实现数量控制
2022/04/08 Redis