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 相关文章推荐
RGB颜色值转HTML十六进制(HEX)代码的JS函数
Apr 25 Javascript
JavaScript constructor和instanceof,JSOO中的一对欢喜冤家
May 25 Javascript
麻雀虽小五脏俱全 Dojo自定义控件应用
Sep 04 Javascript
使用apply方法处理数组的三个技巧[译]
Sep 20 Javascript
JS辨别访问浏览器判断是android还是ios系统
Aug 19 Javascript
jquery.idTabs 选项卡使用示例代码
Sep 03 Javascript
基于jquery实现无限级树形菜单
Mar 22 Javascript
jquery获取table指定行和列的数据方法(当前选中行、列)
Nov 07 Javascript
vue2.0实战之基础入门(1)
Mar 27 Javascript
Node.js中,在cmd界面,进入退出Node.js运行环境的方法
May 12 Javascript
Koa2微信公众号开发之本地开发调试环境搭建
May 16 Javascript
vue中v-show和v-if的异同及v-show用法
Jun 06 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
ThinkPHP模型详解
2015/07/27 PHP
php一个文件搞定微信jssdk配置
2016/12/12 PHP
php成功操作redis cluster集群的实例教程
2019/01/13 PHP
javascript实现原生ajax的几种方法介绍
2013/09/21 Javascript
JavaScript 数组详解
2013/10/10 Javascript
jQuery中slideUp()方法用法分析
2014/12/24 Javascript
JS实现为表格动态添加标题的方法
2015/03/31 Javascript
JS中捕获console.log()输出的方法
2015/04/16 Javascript
js实现跨域的几种方法汇总(图片ping、JSONP和CORS)
2015/10/25 Javascript
js正则表达式验证邮件地址
2015/11/12 Javascript
Angularjs中$http以post请求通过消息体传递参数的实现方法
2016/08/05 Javascript
JavaScript简单验证表单空值及邮箱格式的方法
2017/01/20 Javascript
angular仿支付宝密码框输入效果
2017/03/25 Javascript
webpack构建换肤功能的思路详解
2017/11/27 Javascript
vue.js 嵌套循环、if判断、动态删除的实例
2018/03/07 Javascript
微信小程序 子级页面返回父级并把子级参数带回父级实现方法
2019/08/22 Javascript
使用PreloadJS加载图片资源的基础方法详解
2020/02/03 Javascript
[02:16]卖萌的僵尸 DOTA2神话信使飞僵小宝来袭
2014/03/24 DOTA
[01:27]2014DOTA2展望TI 剑指西雅图IG战队专访
2014/06/30 DOTA
[05:03]2018DOTA2亚洲邀请赛主赛事首日回顾
2018/04/04 DOTA
[01:22:29]真视界:2019年国际邀请赛总决赛
2020/01/29 DOTA
批处理与python代码混合编程的方法
2016/05/19 Python
通过python+selenium3实现浏览器刷简书文章阅读量
2017/12/26 Python
漂亮的Django Markdown富文本app插件的实现
2019/01/02 Python
Python PIL读取的图像发生自动旋转的实现方法
2019/07/05 Python
python3.6+selenium实现操作Frame中的页面元素
2019/07/16 Python
Python实现语音识别和语音合成功能
2019/09/20 Python
什么是Oracle的后台进程background processes?都有哪些后台进程?
2012/04/26 面试题
外语系毕业生求职自荐信
2014/04/12 职场文书
小区门卫的岗位职责
2014/09/26 职场文书
技术股东合作协议书
2014/12/02 职场文书
大客户经理岗位职责
2015/04/09 职场文书
农村结婚典礼主持词
2015/06/29 职场文书
宾馆卫生管理制度
2015/08/06 职场文书
OpenCV实现反阈值二值化
2021/11/17 Java/Android
Python装饰器详细介绍
2022/03/25 Python