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 相关文章推荐
用window.location.href实现刷新另个框架页面
Mar 07 Javascript
javascript hashtable 修正版 下载
Dec 30 Javascript
基于jQuery的图片剪切插件
Aug 03 Javascript
Extjs4 Treegrid 使用心得分享(经验篇)
Jul 01 Javascript
JavaScript的面向对象编程基础
Aug 13 Javascript
利用Jquery队列实现根据输入数量显示的动画
Sep 01 Javascript
js数组方法reduce经典用法代码分享
Jan 07 Javascript
JS 实现微信扫一扫功能
Sep 14 Javascript
vue 内置过滤器的使用总结(附加自定义过滤器)
Dec 11 Javascript
node.js中ws模块创建服务端和客户端,网页WebSocket客户端
Mar 06 Javascript
vue路由传参的基本实现方式小结【三种方式】
Feb 05 Javascript
通过js随机函数Math.random实现乱序
May 19 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获取网址的顶级域名函数代码
2012/09/24 PHP
PHP实现生成唯一会员卡号
2015/08/24 PHP
PHP实现清除wordpress里恶意代码
2015/10/21 PHP
php实现面包屑导航例子分享
2015/12/19 PHP
php常用字符串String函数实例总结【转换,替换,计算,截取,加密】
2016/12/07 PHP
Laravel 错误提示本地化的实现
2019/10/22 PHP
laravel框架实现敏感词汇过滤功能示例
2020/02/15 PHP
js获取对象为null的解决方法
2013/11/21 Javascript
jQuery 文本框得失焦点的简单实例
2014/02/19 Javascript
jQuery经过一段时间自动隐藏指定元素的方法
2015/03/17 Javascript
Node.js中Request模块处理HTTP协议请求的基本使用教程
2016/03/31 Javascript
JQuery Mobile 弹出式登录框的实现方法
2016/05/28 Javascript
jQuery树插件zTree使用方法详解
2017/05/02 jQuery
Javascript获取某个月的天数
2018/05/30 Javascript
Vue触发式全局组件构建的方法
2018/11/28 Javascript
Vue记住滚动条和实现下拉加载的完美方法
2020/07/31 Javascript
[02:09]EHOME夺得首届辉夜杯冠军—现场颁奖仪式
2015/12/28 DOTA
在Django的模型中执行原始SQL查询的方法
2015/07/21 Python
Django中对通过测试的用户进行限制访问的方法
2015/07/23 Python
python字符串,数值计算
2016/10/05 Python
Python Nose框架编写测试用例方法
2017/10/26 Python
django反向解析和正向解析的方式
2018/06/05 Python
pycharm运行和调试不显示结果的解决方法
2018/11/30 Python
Python字符串大小写转换拼接删除空白
2019/09/19 Python
在Python IDLE 下调用anaconda中的库教程
2020/03/09 Python
用python批量移动文件
2021/01/14 Python
浅谈HTML5 服务器推送事件(Server-sent Events)
2017/08/01 HTML / CSS
女装和独特珠宝:Sundance Catalog
2018/09/19 全球购物
在线课程:Skillshare
2019/04/02 全球购物
普师专业个人自荐信范文
2013/11/26 职场文书
弄虚作假心得体会
2014/09/10 职场文书
2014年信息宣传工作总结
2014/12/18 职场文书
作弊检讨书
2015/01/27 职场文书
上课睡觉万能检讨书
2015/02/17 职场文书
签订劳动合同通知书
2015/04/16 职场文书
高中生物教学反思
2016/02/20 职场文书