javascript SpiderMonkey中的函數(shù)序列化如何進行
更新時間:2012年12月05日 11:45:48 作者:
JavaScript中如何進行函數(shù)序列化,函數(shù)序列化的作用是什么?本文將介紹SpiderMonkey中的函數(shù)序列化,有需要的朋友可以參考下
在Javascript中,函數(shù)可以很容易的被序列化(字符串化),也就是得到函數(shù)的源碼.但其實這個操作的內(nèi)部實現(xiàn)(引擎實現(xiàn))并不是你想象的那么簡單.SpiderMonkey中一共使用過兩種函數(shù)序列化的技術(shù):一種是利用反編譯器(decompiler)將函數(shù)編譯后的字節(jié)碼反編譯成源碼字符串,另一種是在將函數(shù)編譯成字節(jié)碼之前就把函數(shù)源碼壓縮并存儲下來,用到的時候再解壓還原.
如何進行函數(shù)序列化
在SpiderMonkey中,能將函數(shù)序列化的方法或函數(shù)有三個:Function.prototype.toString,Function.prototype.toSource,uneval.只有toString方法是標準的,也就是各引擎通用的.但是ES標準中關(guān)于Function.prototype.toString方法的規(guī)定(ES5 15.3.4.2)只有寥寥數(shù)語,也就是說,基本沒有標準,引擎自己決定該如何實現(xiàn).
函數(shù)序列化的作用
函數(shù)序列化最主要的作用應(yīng)該是利用序列化生成的函數(shù)源碼來重新定義這個函數(shù).
function a() {
...
alert("a")
...
}
a() //執(zhí)行時可能會彈出"a"
a = eval("(" + a.toString().replace('alert("a")', 'alert("b")') + ")")
a() //執(zhí)行時可能會彈出"b"
你也許會想:"我寫了這么多年Javascript,怎么沒有遇到這種需求".的確,如果是自己的網(wǎng)站,自己完全控制的js文件,不需要以這種打補丁的方式來修改函數(shù),直接修改就可以了.但是如果源文件不是你能控制的了的話,就很有可能要這樣做了.比如常用的地方有g(shù)reasemonkey腳本:你可能需要禁用或修改某個網(wǎng)站中的某個函數(shù).還有就是Firefox擴展:你需要修改Firefox自身的某個函數(shù)(可以說Firefox是用JS寫的).舉個我自己寫的Firefox腳本的例子:
這個代碼的作用是:在地址欄上回車時,讓Firefox在新標簽中打開頁面,而不是占用當前標簽.實現(xiàn)方式就是用toString方法讀取到gURLBar.handleCommand函數(shù)的源碼,然后用正則替換后傳給eval,重新定義了這個函數(shù).
為什么不用直接定義的方式,也就是直接重寫函數(shù)呢:
gURLBar.handleCommand = function(){...//將原本的函數(shù)更改了一個小地方}
不能這么做的原因是因為我們得考慮兼容性,我們應(yīng)該盡可能小的更改這個函數(shù)的源碼.如果這么寫的話,Firefox的gURLBar.handleCommand源碼一旦發(fā)生變化,這個腳本就失效了.比如Firefox3和Firefox4中都有這個函數(shù),但函數(shù)內(nèi)容差別非常大,可是如果用正則替換部分關(guān)鍵字的話,只要這個被替換的這個關(guān)鍵字沒有發(fā)生變化的話,就不會出現(xiàn)不兼容的現(xiàn)象.
反編譯字節(jié)碼
在SpiderMonkey中,函數(shù)在被解析之后會被編譯成字節(jié)碼(bytecode),也就是說,內(nèi)存中存儲著并不是原始的函數(shù)源碼.SpiderMonkey中存在一個反編譯器,它的主要作用就是把函數(shù)的字節(jié)碼反編譯成函數(shù)源碼的形式.
在Firefox16以及之前的版本中,SpiderMonkey使用的就是這種方法,如果你使用的是這些版本的Firefox的話,可以嘗試下面的代碼:
alert(function () {
"字符串";
//注釋
return 1 + 2 + 3
}.toString())
返回的字符串是
function () {
return 6;
}
輸出和其他的瀏覽器完全不同:
1.沒有意義的原始值字面量在編譯的時候會被刪除,這個例子中就是"字符串".
你也許會覺得:"貌似沒什么問題,反正這些值對于函數(shù)的運行來說并沒有什么意義".等等,你是不是忘了個東西,表示嚴格模式的字符串"use strict"怎么辦呢?
在不支持嚴格模式的版本中,比如Firefox3.6,這個"use strict"和其他字符串沒什么區(qū)別,編譯的時候會被刪除.在SpiderMonkey實現(xiàn)了嚴格模式之后,雖然編譯的時候同樣會忽略掉這個字符串"use strict",但在反編譯的時候會進行判斷,如果這個函數(shù)處于嚴格模式中,則會在函數(shù)體的第一行添加上"use strict",下面是對應(yīng)的引擎源碼.
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.注釋在編譯的時候也會被刪除
這個貌似沒太大影響,不過有些人愿意利用函數(shù)注釋來實現(xiàn)多行字符串,這個方法在Firefox 17之前的版本中是不可用的.
function hereDoc(f) {
return f.toString().replace(/^.+\s/,"").replace(/.+$/,"");
}
var string = hereDoc(function () {/*
我
你
他
*/});
console.log(string)
我
你
他
3.原始值字面量的運算會在編譯時進行.
這算是一種優(yōu)化方式,《高性能JavaScript》提到過:

反編譯的弊端
由于新技術(shù)的出現(xiàn)(比如嚴格模式)以及在修改其他相關(guān)bug的時候,反編譯器這部分的實現(xiàn)經(jīng)常需要更改,更改就有可能產(chǎn)生新的bug,我自己就親身遇到過一個bug.大概是在Firefox10左右的時候,具體問題記不大清了,反正是關(guān)于反編譯時小括號是否要保留的問題,大概是這樣的:
>(function (a,b,c){return (a+b)+c}).toString()
"function (a, b, c) {
return a + b + c;
}"
在反編譯時,(a+b)中的小括號被省略了,由于加法結(jié)合律從左到右,所以這沒關(guān)系.但我遇到的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".
關(guān)于反編譯器,Mozilla工程師Luke Wagner指出,反編譯器對他們實現(xiàn)一些新功能的阻礙很大,而且經(jīng)常會出現(xiàn)一些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. 存儲函數(shù)源碼
從Firefox17之后,SpiderMonkey改成了第二種實現(xiàn)方法,其他瀏覽器也應(yīng)該是這樣實現(xiàn)的吧.函數(shù)序列化得到的字符串完全和源碼一致,包括空白符,注釋等等.這樣的話,大部分問題就應(yīng)該沒有了吧.不過,貌似我又想到個問題.還是關(guān)于嚴格模式的.
比如:
(function A() {
"use strict";
alert("A");
}) + ""
當然,返回的源碼中也應(yīng)該有"use strict",所有瀏覽器都是這么實現(xiàn)的:
function A() {
"use strict";
alert("A");
}
但如果是這樣呢:
(function A() {
"use strict";
return function B() {
alert("B")
}
})() + ""
內(nèi)部函數(shù)B也處于嚴格模式中,輸出B的函數(shù)源碼應(yīng)不應(yīng)該加上"use strict"呢.試驗一下:
上面說了,Firefox17之前Firefox4之后的版本是通過判斷當前函數(shù)是否處于嚴格模式來決定輸出不輸出"use strict"的,函數(shù)B繼承了函數(shù)A的嚴格模式,所以會有"use strict".
同時函數(shù)源碼是縮進嚴格的,因為在反編譯的時候,SpiderMonkey會給反編譯出的源碼進行格式化,即使之前的源碼完全沒有縮進也沒關(guān)系:
function B() {
"use strict";
alert("B");
}
Firefox17之后的版本會不會帶有"use strict"呢?因為是直接把函數(shù)源碼保存下來的,而且函數(shù)B中的確沒有"use strict"字樣.試驗結(jié)果是:會添加上"use strict",只是縮進有點問題,因為沒有格式化這一步了.
function B() {
"use strict";
alert("B")
}
SpiderMonkey最新版的jsfun.cpp源碼中有對應(yīng)的注釋
// 如果一個函數(shù)的某個上層函數(shù)中擁有"use strict",那么這個函數(shù)就繼承了上層函數(shù)的嚴格模式.
// 我們也會在這個內(nèi)部函數(shù)的函數(shù)體內(nèi)插入"use strict".
// 這就確保了,如果這個函數(shù)的toString方法的返回值被重新求值時,
// 重新生成的函數(shù)會和原函數(shù)有著相同的語義.
而不同的是,其他瀏覽器都是不帶"use strict"的:
function B() {
alert("B")
}
雖然這不會有什么太大影響,但我覺的Firefox的實現(xiàn)是更合理的.
如何進行函數(shù)序列化
在SpiderMonkey中,能將函數(shù)序列化的方法或函數(shù)有三個:Function.prototype.toString,Function.prototype.toSource,uneval.只有toString方法是標準的,也就是各引擎通用的.但是ES標準中關(guān)于Function.prototype.toString方法的規(guī)定(ES5 15.3.4.2)只有寥寥數(shù)語,也就是說,基本沒有標準,引擎自己決定該如何實現(xiàn).
函數(shù)序列化的作用
函數(shù)序列化最主要的作用應(yīng)該是利用序列化生成的函數(shù)源碼來重新定義這個函數(shù).
復(fù)制代碼 代碼如下:
function a() {
...
alert("a")
...
}
a() //執(zhí)行時可能會彈出"a"
a = eval("(" + a.toString().replace('alert("a")', 'alert("b")') + ")")
a() //執(zhí)行時可能會彈出"b"
你也許會想:"我寫了這么多年Javascript,怎么沒有遇到這種需求".的確,如果是自己的網(wǎng)站,自己完全控制的js文件,不需要以這種打補丁的方式來修改函數(shù),直接修改就可以了.但是如果源文件不是你能控制的了的話,就很有可能要這樣做了.比如常用的地方有g(shù)reasemonkey腳本:你可能需要禁用或修改某個網(wǎng)站中的某個函數(shù).還有就是Firefox擴展:你需要修改Firefox自身的某個函數(shù)(可以說Firefox是用JS寫的).舉個我自己寫的Firefox腳本的例子:
復(fù)制代碼 代碼如下:
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在新標簽中打開頁面,而不是占用當前標簽.實現(xiàn)方式就是用toString方法讀取到gURLBar.handleCommand函數(shù)的源碼,然后用正則替換后傳給eval,重新定義了這個函數(shù).
為什么不用直接定義的方式,也就是直接重寫函數(shù)呢:
gURLBar.handleCommand = function(){...//將原本的函數(shù)更改了一個小地方}
不能這么做的原因是因為我們得考慮兼容性,我們應(yīng)該盡可能小的更改這個函數(shù)的源碼.如果這么寫的話,Firefox的gURLBar.handleCommand源碼一旦發(fā)生變化,這個腳本就失效了.比如Firefox3和Firefox4中都有這個函數(shù),但函數(shù)內(nèi)容差別非常大,可是如果用正則替換部分關(guān)鍵字的話,只要這個被替換的這個關(guān)鍵字沒有發(fā)生變化的話,就不會出現(xiàn)不兼容的現(xiàn)象.
反編譯字節(jié)碼
在SpiderMonkey中,函數(shù)在被解析之后會被編譯成字節(jié)碼(bytecode),也就是說,內(nèi)存中存儲著并不是原始的函數(shù)源碼.SpiderMonkey中存在一個反編譯器,它的主要作用就是把函數(shù)的字節(jié)碼反編譯成函數(shù)源碼的形式.
在Firefox16以及之前的版本中,SpiderMonkey使用的就是這種方法,如果你使用的是這些版本的Firefox的話,可以嘗試下面的代碼:
復(fù)制代碼 代碼如下:
alert(function () {
"字符串";
//注釋
return 1 + 2 + 3
}.toString())
返回的字符串是
function () {
return 6;
}
輸出和其他的瀏覽器完全不同:
1.沒有意義的原始值字面量在編譯的時候會被刪除,這個例子中就是"字符串".
你也許會覺得:"貌似沒什么問題,反正這些值對于函數(shù)的運行來說并沒有什么意義".等等,你是不是忘了個東西,表示嚴格模式的字符串"use strict"怎么辦呢?
在不支持嚴格模式的版本中,比如Firefox3.6,這個"use strict"和其他字符串沒什么區(qū)別,編譯的時候會被刪除.在SpiderMonkey實現(xiàn)了嚴格模式之后,雖然編譯的時候同樣會忽略掉這個字符串"use strict",但在反編譯的時候會進行判斷,如果這個函數(shù)處于嚴格模式中,則會在函數(shù)體的第一行添加上"use strict",下面是對應(yīng)的引擎源碼.
static JSBool
復(fù)制代碼 代碼如下:
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.注釋在編譯的時候也會被刪除
這個貌似沒太大影響,不過有些人愿意利用函數(shù)注釋來實現(xiàn)多行字符串,這個方法在Firefox 17之前的版本中是不可用的.
復(fù)制代碼 代碼如下:
function hereDoc(f) {
return f.toString().replace(/^.+\s/,"").replace(/.+$/,"");
}
var string = hereDoc(function () {/*
我
你
他
*/});
console.log(string)
我
你
他
3.原始值字面量的運算會在編譯時進行.
這算是一種優(yōu)化方式,《高性能JavaScript》提到過:

反編譯的弊端
由于新技術(shù)的出現(xiàn)(比如嚴格模式)以及在修改其他相關(guān)bug的時候,反編譯器這部分的實現(xiàn)經(jīng)常需要更改,更改就有可能產(chǎn)生新的bug,我自己就親身遇到過一個bug.大概是在Firefox10左右的時候,具體問題記不大清了,反正是關(guān)于反編譯時小括號是否要保留的問題,大概是這樣的:
復(fù)制代碼 代碼如下:
>(function (a,b,c){return (a+b)+c}).toString()
"function (a, b, c) {
return a + b + c;
}"
在反編譯時,(a+b)中的小括號被省略了,由于加法結(jié)合律從左到右,所以這沒關(guān)系.但我遇到的bug是這樣的:
復(fù)制代碼 代碼如下:
>(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".
關(guān)于反編譯器,Mozilla工程師Luke Wagner指出,反編譯器對他們實現(xiàn)一些新功能的阻礙很大,而且經(jīng)常會出現(xiàn)一些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. 存儲函數(shù)源碼
從Firefox17之后,SpiderMonkey改成了第二種實現(xiàn)方法,其他瀏覽器也應(yīng)該是這樣實現(xiàn)的吧.函數(shù)序列化得到的字符串完全和源碼一致,包括空白符,注釋等等.這樣的話,大部分問題就應(yīng)該沒有了吧.不過,貌似我又想到個問題.還是關(guān)于嚴格模式的.
比如:
復(fù)制代碼 代碼如下:
(function A() {
"use strict";
alert("A");
}) + ""
當然,返回的源碼中也應(yīng)該有"use strict",所有瀏覽器都是這么實現(xiàn)的:
復(fù)制代碼 代碼如下:
function A() {
"use strict";
alert("A");
}
但如果是這樣呢:
復(fù)制代碼 代碼如下:
(function A() {
"use strict";
return function B() {
alert("B")
}
})() + ""
內(nèi)部函數(shù)B也處于嚴格模式中,輸出B的函數(shù)源碼應(yīng)不應(yīng)該加上"use strict"呢.試驗一下:
上面說了,Firefox17之前Firefox4之后的版本是通過判斷當前函數(shù)是否處于嚴格模式來決定輸出不輸出"use strict"的,函數(shù)B繼承了函數(shù)A的嚴格模式,所以會有"use strict".
同時函數(shù)源碼是縮進嚴格的,因為在反編譯的時候,SpiderMonkey會給反編譯出的源碼進行格式化,即使之前的源碼完全沒有縮進也沒關(guān)系:
復(fù)制代碼 代碼如下:
function B() {
"use strict";
alert("B");
}
Firefox17之后的版本會不會帶有"use strict"呢?因為是直接把函數(shù)源碼保存下來的,而且函數(shù)B中的確沒有"use strict"字樣.試驗結(jié)果是:會添加上"use strict",只是縮進有點問題,因為沒有格式化這一步了.
復(fù)制代碼 代碼如下:
function B() {
"use strict";
alert("B")
}
SpiderMonkey最新版的jsfun.cpp源碼中有對應(yīng)的注釋
// 如果一個函數(shù)的某個上層函數(shù)中擁有"use strict",那么這個函數(shù)就繼承了上層函數(shù)的嚴格模式.
// 我們也會在這個內(nèi)部函數(shù)的函數(shù)體內(nèi)插入"use strict".
// 這就確保了,如果這個函數(shù)的toString方法的返回值被重新求值時,
// 重新生成的函數(shù)會和原函數(shù)有著相同的語義.
而不同的是,其他瀏覽器都是不帶"use strict"的:
復(fù)制代碼 代碼如下:
function B() {
alert("B")
}
雖然這不會有什么太大影響,但我覺的Firefox的實現(xiàn)是更合理的.
相關(guān)文章
關(guān)于javascript中的parseInt使用技巧
前面好幾次遇到JavaScript中parseInt函數(shù)把字串轉(zhuǎn)為數(shù)字,前面遇到過幾次問題,然后就換其它方法進行比較。2009-09-09JavaScript Break 和 Continue區(qū)別教程
JavaScript Break 和 Continue區(qū)別教程...2007-04-04基于JavaScript自定義構(gòu)造函數(shù)的詳解說明
本篇文章小編為大家介紹,基于JavaScript自定義構(gòu)造函數(shù)的詳解說明。需要的朋友參考下2013-04-04Mobile Web開發(fā)基礎(chǔ)之四--處理手機設(shè)備的橫豎屏問題
這篇文章主要介紹了Mobile Web開發(fā)基礎(chǔ)之-—處理手機設(shè)備的橫豎屏,window.orientation屬性與onorientationchange事件以及media query方式是開發(fā)過程中需要注意到的兩種解決方式,需要的朋友可以參考下2017-08-08