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 相关文章推荐
ASP中用Join和Array,可以加快字符连接速度的代码
Aug 22 Javascript
JavaScript CSS修改学习第三章 修改样式表
Feb 19 Javascript
解析JavaScript中点号“.”的多义性
Dec 02 Javascript
JavaScript实现的简单烟花特效代码
Oct 20 Javascript
JS实现物体带缓冲的间歇运动效果示例
Dec 22 Javascript
Vue.js搭建移动端购物车界面
Jun 28 Javascript
Node.js中DNS模块学习总结
Feb 28 Javascript
微信小程序开发之路由切换页面重定向问题
Sep 18 Javascript
纯javascript实现选择框的全选与反选功能
Apr 08 Javascript
Vue的路由及路由钩子函数的实现
Jul 02 Javascript
原生js实现文件上传、下载、封装等实例方法
Jan 05 Javascript
基于javascript的无缝滚动动画1
Aug 07 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
全国FM电台频率大全 - 15 山东省
2020/03/11 无线电
PHP生成静态页
2006/11/25 PHP
JQuery 将元素显示在屏幕的中央的代码
2010/02/27 Javascript
使用JS或jQuery模拟鼠标点击a标签事件代码
2014/03/10 Javascript
js获取会话框prompt的返回值的方法
2015/01/10 Javascript
jQuery实现将div中滚动条滚动到指定位置的方法
2016/08/10 Javascript
JS中的数组转变成JSON格式字符串的方法
2017/05/09 Javascript
for循环 + setTimeout 结合一些示例(前端面试题)
2017/08/30 Javascript
JS实现点击复选框变更DIV显示状态的示例代码
2017/12/18 Javascript
JavaScript创建对象的常用方式总结
2018/08/10 Javascript
在vue.js中使用JSZip实现在前端解压文件的方法
2018/09/05 Javascript
微信小程序rich-text富文本用法实例分析
2019/05/20 Javascript
createObjectURL方法实现本地图片预览
2019/09/30 Javascript
JS数组方法shift()、unshift()用法实例分析
2020/01/18 Javascript
Vue中import from的来源及省略后缀与加载文件夹问题
2020/02/09 Javascript
小程序跳转H5页面的方法步骤
2020/03/06 Javascript
vue中使用带隐藏文本信息的图片、图片水印的方法
2020/04/24 Javascript
菜鸟使用python实现正则检测密码合法性
2016/01/05 Python
Python减少循环层次和缩进的技巧分析
2016/03/15 Python
Python网络编程之TCP套接字简单用法示例
2018/04/09 Python
python对html过滤处理的方法
2018/10/21 Python
对Python 获取类的成员变量及临时变量的方法详解
2019/01/22 Python
Python imutils 填充图片周边为黑色的实现
2020/01/19 Python
使用Django搭建网站实现商品分页功能
2020/05/22 Python
解决python运行启动报错问题
2020/06/01 Python
CSS3 真的会替代 SCSS 吗
2021/03/09 HTML / CSS
银行实习自我鉴定
2013/10/12 职场文书
人力资源管理专业毕业生推荐信
2013/11/07 职场文书
会展策划与管理专业大学生职业生涯规划
2014/02/07 职场文书
校园游戏活动新闻稿
2014/10/15 职场文书
年终工作总结范文2014
2014/11/27 职场文书
同学聚会通知书
2015/04/20 职场文书
城镇居民医疗保险工作总结
2015/08/10 职场文书
详解python的内存分配机制
2021/05/10 Python
MySQL令人大跌眼镜的隐式转换
2021/08/23 MySQL
SpringBoot2零基础到精通之数据库专项精讲
2022/03/22 Java/Android