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 相关文章推荐
javascript web对话框与弹出窗口
Feb 22 Javascript
麻雀虽小五脏俱全 Dojo自定义控件应用
Sep 04 Javascript
Javascript表格翻页效果的具体实现
Oct 05 Javascript
JQuery验证jsp页面属性是否为空(实例代码)
Nov 08 Javascript
Underscore.js常用方法总结
Feb 28 Javascript
高性能JavaScript DOM编程(1)
Aug 11 Javascript
jQuery使用$.ajax提交表单完整实例
Dec 11 Javascript
vue组件之间通信实例总结(点赞功能)
Dec 05 Javascript
详解单页面路由工程使用微信分享及二次分享解决方案
Feb 22 Javascript
jquery实现Ajax请求的几种常见方式总结
May 28 jQuery
在layui下对元素进行事件绑定的实例
Sep 06 Javascript
通过实例了解JS 连续赋值
Sep 24 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
Ubuntu12下编译安装PHP5.3开发环境
2015/03/27 PHP
php使用ffmpeg获取视频信息并截图的实现方法
2016/05/03 PHP
PHP hex2bin()函数用法讲解
2019/02/25 PHP
js文件包含的几种方式介绍
2014/09/28 Javascript
JavaScript中的Truthy和Falsy介绍
2015/01/01 Javascript
JS实现转动随机数抽奖特效代码
2020/04/16 Javascript
jQuery中prepend()方法使用详解
2015/08/11 Javascript
鼠标悬停小图标显示大图标
2016/01/22 Javascript
如何提高数据访问速度
2016/12/26 Javascript
js 调用百度分享功能
2017/02/27 Javascript
js中的 || 与 && 运算符详解
2018/05/24 Javascript
vue树形结构获取键值的方法示例
2018/06/21 Javascript
vue2.0实现的tab标签切换效果(内容可自定义)示例
2019/02/11 Javascript
微信小程序实现元素渐入渐出动画效果封装方法
2019/05/18 Javascript
javascript实现画板功能
2020/04/12 Javascript
前端性能优化建议
2020/09/17 Javascript
解决vue打包报错Unexpected token: punc的问题
2020/10/24 Javascript
Vue实现指令式动态追加小球动画组件的步骤
2020/12/18 Vue.js
javascript中闭包closure的深入讲解
2021/03/03 Javascript
Python算法之栈(stack)的实现
2014/08/18 Python
Python实现的科学计算器功能示例
2017/08/04 Python
python Celery定时任务的示例
2018/03/13 Python
Python分支语句与循环语句应用实例分析
2019/05/07 Python
Python 私有化操作实例分析
2019/11/21 Python
在tensorflow下利用plt画论文中loss,acc等曲线图实例
2020/06/15 Python
python操作微信自动发消息的实现(微信聊天机器人)
2020/07/14 Python
解决img标签上下出现间隙的方法
2016/12/14 HTML / CSS
分享全球十款超强HTML5开发工具
2014/05/14 HTML / CSS
缅甸网上购物:Shop.com.mm
2017/12/05 全球购物
澳大利亚儿童精品仓库:Goo & Co.
2019/06/20 全球购物
幼儿园实习生辞职信
2014/01/20 职场文书
建设投标担保书
2014/05/13 职场文书
党的群众路线教育实践活动个人对照检查材料(企业)
2014/11/05 职场文书
2014年幼儿园教研工作总结
2014/12/04 职场文书
2015年双拥工作总结
2015/04/08 职场文书
工商局调档介绍信
2015/10/22 职场文书