在Javascript中,函數可以很容易的被序列化(字串化),也就是得到函數的源碼.但其實這個操作的內部實現(引擎實現)並不是你想像的那麼簡單.SpiderMonkey中一共使用過兩種函數序列化的技術:一種是利用反編譯器(decompiler)將函數編譯後的字節碼反編譯成源碼字串,另一種是在將函數編譯成字節碼之前就把函數源碼壓縮並且儲存下來,用到的時候再解壓縮還原.
如何進行函數序列化
在SpiderMonkey中,能將函數序列化的方法或函數有三個:Function. prototype.toString,Function.prototype.toSource,uneval.只有toString方法是標準的,也就是各引擎通用的.但是ES標準中關於Function.prototype.toString方法的規定(ES5 15.3.4.2)只有寥寥數語,也就是說,基本上沒有標準,引擎自己決定該如何實現.
函數序列化的作用
函數序列化最主要的作用應該是利用序列化生成的函數原始碼重新定義這個函數.
function(
...
alert("a")
...
}
a() //執行時可能會彈出"a"
a = eval("(" a.toString().replace('alert("a")', 'alert("b")') ")")
a() //執行時可能會彈出"b"
你也許會想:"我寫了這麼多年Javascript,怎麼沒有遇到這種需求".的確,如果是自己的網站,自己完全控制的js文件,不需要以這種打補丁的方式來修改函數,直接修改就可以了.但是如果源文件不是你能控制的了的話,就很有可能要這樣做了.比如常用的地方有greasemonkey腳本:你可能需要停用或修改某個網站中的某個函數.還有就是Firefox擴充:你需要修改Firefox自身的某個函數(可以說Firefox是用JS寫的).舉個我自己寫的
Firefox腳本的範例:
location == "🎜> 程式碼如下:
location == " /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(>js_printf(>js_printf(>js_printf( jp, "t"use strict";n");
}
jp->strict = true;
2.註解在編譯的時候也會刪除
這個貌似沒太大影響,不過有些人願意利用函數註解來實現多行字串,這個方法在Firefox 17之前的版本中是不可用的.
複製程式碼
程式碼如下:
function hereDoc(f) {
return f.toString().replace(/^. s { /,"").replace(/. $/,"");
} var string = hereDoc(function () {/*
我
你
他
*/});
console.log(string)
我
你
反編譯的弊端
由於新技術的出現(例如嚴格模式)以及在修改其他相關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 this the starter fuzz bugs.The sooner we remove this this the starter we 也, 零, this. 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() {
(function A() {
(function A() {
(function A() {
當然,傳回的來源碼中也應該有"use strict",所有瀏覽器都是這麼實現的:
複製程式碼
程式碼如下:
alert("A");
}
但如果是這樣呢:
複製程式碼
程式碼如下:
(function A() {
內部函數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 ",那麼這個函數就繼承了上層函數的嚴格模式.
// 我們也會在這個內部函數的函數體內插入"use strict".
// 這就確保了,如果這個函數的toString方法的回傳值被重新求值時,
// 重新產生的函數會和原函數有著相同的語意.
而不同的是,其他瀏覽器都是不帶" use strict"的: 複製程式碼 程式碼如下: function B() >alert("B") } 雖然這不會有什麼太大影響,但我覺的Firefox的實現是更合理的.