这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
在 Javascript 中, 函数可以很容易的被序列化 (字符串化), 也就是得到函数的源码. 但其实这个操作的内部实现(引擎实现) 并不是你想象的那么简单. SpiderMonkey 中一共使用过两种函数序列化的技术: 一种是利用反编译器 (decompiler) 将函数编译后的字节码反编译成源码字符串, 另一种是在将函数编译成字节码之前就把函数源码压缩并存储下来, 用到的时候再解压还原.
JavaScript 中如何进行函数序列化,函数序列化的作用是什么?本文将介绍 SpiderMonkey 中的函数序列化,有需要的朋友可以参考下
如何进行函数序列化在 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
2. 注释在编译的时候也会被删除
- 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);
- }
这个貌似没太大影响, 不过有些人愿意利用函数注释来实现多行字符串, 这个方法在 Firefox 17 之前的版本中是不可用的.
- function hereDoc(f) {
- return f.toString().replace(/^.+\s/, "").replace(/.+$/, "");
- }
- var string = hereDoc(function() {
- /*
- 我
- 你
- 他
- */
- });
- console.log(string)
我 你 他
3. 原始值字面量的运算会在编译时进行. 这算是一种优化方式,《高性能 JavaScript》提到过:
反编译的弊端由于新技术的出现 (比如严格模式) 以及在修改其他相关 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 的实现是更合理的.
来源: http://www.phperz.com/article/17/0415/281385.html