javas命名函數(shù)表達(dá)式探秘_第1頁
javas命名函數(shù)表達(dá)式探秘_第2頁
javas命名函數(shù)表達(dá)式探秘_第3頁
javas命名函數(shù)表達(dá)式探秘_第4頁
javas命名函數(shù)表達(dá)式探秘_第5頁
已閱讀5頁,還剩14頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

1、命名函數(shù)表達(dá)式探秘作者:Juriy kangax Zaytsev ()譯者:為之漫筆 ()時間:2009年6月17日 最近修改:2009年10月9日翻譯時間:2009年12月9日 修訂時間:2009年12月22日..5.前言 (#introduction)函數(shù)表達(dá)式與函數(shù)(#expr-vs-decl) 函數(shù)語句 (#function-statements)命名函數(shù)表達(dá)式 (#named-expr)調(diào)試器中的函數(shù)名 (#names-in-debuggers)JScript的bug (#jscript-bugs)JScript的內(nèi)存管

2、理 (#jscript-memory-management)測試 (#tests)Safari中存在的bug (#safari-bug)SpiderMonkey的怪癖 (#spidermonkey-peculiarity) 解決方案 (#solution)替代方案 (#alt-solution)WebKit的displayName (#webkit-displayName)對未來的思考 (#future-considerations) 致謝 (#credits)前言我覺得很奇怪,網(wǎng)上好像一直沒有人認(rèn)真地討論過命名函數(shù)表達(dá)式(Named Function Expression,即“有名字函數(shù)表達(dá)

3、式”,與“函數(shù)”相對。譯者注)。而這也許正是各種各樣的誤解隨處可見的一個。在這篇文章里,我打算從理論和實踐兩個方面出發(fā),對這些令人驚嘆的JavaScript結(jié)構(gòu)的優(yōu)缺點給出一個結(jié)論。簡單來講,命名函數(shù)表達(dá)式只有一個用處在調(diào)試器或性能分析程序中描述函數(shù)的名稱。沒錯,也可以使用函數(shù)名實現(xiàn)遞歸, 但你很快就會知道,目前來看這通常是不切實際的。當(dāng)然,如果你不關(guān)注調(diào)試,那就沒什么可擔(dān)心的。否則,就應(yīng)該往下看一看,看看在跨瀏覽器開發(fā)中都會出現(xiàn)哪些小毛病(glitch),也看看應(yīng)該怎樣解決它們。一開始呢,我會先介紹一下什么是函數(shù)表達(dá)式,以及現(xiàn)代調(diào)試器如何處理它們之類的內(nèi)容。要是你比較心急,請直接跳到“最終方

4、案” (#solution)部分,該部分詳細(xì)說明了怎樣才能安全地使用這些結(jié)構(gòu)。函數(shù)表達(dá)式與函數(shù)在ECMAScript中,有兩個最常用的創(chuàng)建函數(shù)對象的方法,即使用函數(shù)表達(dá)式或者使用函數(shù)。這兩種方法之間的區(qū)別可謂 相當(dāng)?shù)亓钊死Щ?;至少我是相?dāng)?shù)乩Щ蟆Υ?,ECMA規(guī)范只明確了一點,即函數(shù)也就是函數(shù)名唄,而函數(shù)表達(dá)式 則可省略這個標(biāo)識符:必須始終帶有一個標(biāo)識符(Identifier)函數(shù):function Identifier ( FormalParameterList opt ) FunctionBody 函數(shù)表達(dá)式:function Identifier opt ( FormalParamete

5、rList opt ) FunctionBody 顯然,在省略標(biāo)識符的情況下, “表達(dá)式” 也就只能是表達(dá)式了。可要是不省略標(biāo)識符呢?誰知道它是一個函數(shù),還是一個函數(shù)表達(dá)式畢竟,這種情況下二者是完全一樣的?。繉嵺`表明,ECMAScript是通過上下文來區(qū)分這兩者的:假如 function foo() 是一個賦值表達(dá)式的一部分,則認(rèn)為它是一個函數(shù)表達(dá)式。而如果 function foo() 被包含在一個函數(shù)體內(nèi),或者位于程序(的最上層)中,則將它作為一個函數(shù)來。function foo(); /,因為它是程序的一部分var bar = function foo(); / 表達(dá)式,因為它是賦值表達(dá)

6、式(AssignmentExpression)的一部分new function bar(); / 表達(dá)式,因為它是New表達(dá)式(NewExpression)的一部分(function()function bar(); /)();,因為它是函數(shù)體(FunctionBody)的一部分還有一種不那么顯而易見的函數(shù)表達(dá)式,就是被包含在一對圓括號中的函數(shù) (function foo()。將這種形式看成表達(dá)式同樣是因為上下文的關(guān)系:(和)一個分組操作符,而分組操作符只能包含表達(dá)式:下面再多看幾個例子吧:function foo(); / 函數(shù)(function foo(); / 函數(shù)表達(dá)式:注意它被包含在

7、分組操作符中try (var x = 5); / 分組操作符只能包含表達(dá)式,不能包含語句(這里的var就是語句) catch(err) / SyntaxError(因為“var x = 5”是一個語句,而不是表達(dá)式對表達(dá)式求值必須返回值,但對語句求值則未必返回不知道大家有沒有印象,在使用 eval 對JSON求值的時候,JSON字符串通常是被包含在一對圓括號中的 eval( + json+ )。這樣做的當(dāng)然也不例外分組操作符,也就是那對圓括號,會導(dǎo)致器強(qiáng)制將JSON的花括號當(dāng)成表達(dá)式而不代碼塊來:try x: 5 ; / 和會被作為塊來 catch(err) / SyntaxError(“x:

8、5”只是構(gòu)建對象字面量的語法,但該語法不能出現(xiàn)在外部的語句塊中。譯者注)( x: 5 ); / 分組操作符會導(dǎo)致器強(qiáng)制將和作為對象字面量來和表達(dá)式的行為存在著十分微妙而又十分重要的差別。首先,函數(shù)會在任何表達(dá)式被和求值之前先行被和求值。即使位于源代碼中的最后一行,它也會先于同一作用域中位于最前面的表達(dá)式被求值。還是看個例子更容易理解。在下面這個例子中,函數(shù) fn 是在 alert 后面alert 執(zhí)行的時候,fn已經(jīng)有定義了:的。但是,在alert(fn();function fn() return Hello world!;函數(shù)還有另外一個重要的特點,即通過條件語句函數(shù)的行為并未標(biāo)準(zhǔn)化,因此

9、不同環(huán)境下可能會得到不同的結(jié)果。有鑒于此,奉勸大家千萬不要在條件語句中使用函數(shù),而要使用函數(shù)表達(dá)式。/千萬不要這樣做!有的瀏覽器會把foo為返回first的那個函數(shù)而有的瀏覽器則會讓foo返回secondif(true) function returnfoo() first;else function returnfoo();foo() second;/ 記住,這種情況下要使用函數(shù)表達(dá)式:var foo; if (true) foo = function() return first;else foo = function() return second;foo();想知道使用函數(shù)的實際規(guī)則到

10、底是什么?繼續(xù)往下看吧。嗯,有人不想知道?那請?zhí)^下面這段摘錄的文字。FunctionDeclaration(函數(shù))只能出現(xiàn)在Program(程序)或FunctionBody(函數(shù)體)內(nèi)。從句法上講,它們 不能出現(xiàn)在Block( 塊)( . )中,例如不能出現(xiàn)在 if、while 或 for 語句中。因為 Block(塊) 中只能包含Statement(語句), 而不能包含F(xiàn)unctionDeclaration(函數(shù))這樣的SourceElement(源元素)。另一方面,仔細(xì)看一看產(chǎn)生規(guī)則也會發(fā)現(xiàn),唯一可能讓Expression(表達(dá)式)出現(xiàn)在Block(塊)中情形,就是讓它作為Express

11、ionStatement(表達(dá)式語句)的一 部分。但是,規(guī)范明確規(guī)定了ExpressionStatement(表達(dá)式語句)不能以關(guān)鍵字function開頭。而這實際上就是說,F(xiàn)unctionExpression(函數(shù)表達(dá)式)同樣也不能出現(xiàn)在Statement(語句)或Block(塊)中(別忘了Block(塊)就是由Statement(語句)的)。由于存在上述限制,只要函數(shù)出現(xiàn)在塊中(像上面例子中那樣),實際上就應(yīng)該將其看作一個語法錯誤,而不是什么函數(shù)或表達(dá)式。但問題是,我還沒見過哪個實現(xiàn)是按照上述規(guī)則來這些函數(shù)的;好像每個實現(xiàn)都有的一套。有必要提醒大家一點,根據(jù)規(guī)范的描述,實現(xiàn)可以引入語法擴(kuò)展

12、(見第16部分),只不過任何情況下都不能規(guī)定。而目前來把它們提升到封閉作用域的頂?shù)闹T多客戶端也正是照此辦理的。其中有一些會把塊中的函數(shù)部;另一些則引入了不同的語義并采用了稍復(fù)雜一些的規(guī)則。當(dāng)作一般的函數(shù)函數(shù)語句在諸如此類的對ECMAScript的語法擴(kuò)展中,有一項就是函數(shù)語句,基于Gecko的瀏覽器(在Mac OS X平臺的Firefox1-3.7a1pre中測試過)目前都實現(xiàn)了該項擴(kuò)展。不知道為什么,很多人好像都不知道這項擴(kuò)展,也就更談不上對其優(yōu)劣的評價了(MDC( Mozilla Dev eloper Center, Mozilla開發(fā)者中心) 提到過這個問題 (/Core JavaScr

13、ipt 1.5 Reference:Functions#Conditionally defining a function),但是只有那么三言兩語)。請大家記住,我好奇心的態(tài)度來討論函數(shù)語句的。因此,除非你只基于Gecko的環(huán)境編寫們是抱著學(xué)習(xí)和滿足使用這個擴(kuò)展。,否則我不建議你閑話少說,下面我們就來看看這些非標(biāo)準(zhǔn)的結(jié)構(gòu)有哪些特點:1.一般語句可以出現(xiàn)的地方,函數(shù)語句也可以出現(xiàn)。當(dāng)然包括塊中:if (true) function f() else function f() 2.函數(shù)語句可以像其他語句一樣被,包含基于條件執(zhí)行的情形:if (true) function foo() return

14、1; else function foo() return 2; foo(); / 1/注意其他類型的客戶端會把這里的foo為函數(shù)因此,第二個foo會覆蓋第一個,結(jié)果返回2而不返回13.的,而是在運(yùn)行時的與函數(shù)表達(dá)式一樣。不過,一旦函數(shù)語句不是在變量初始化期間,函數(shù)語句的標(biāo)識符就在函數(shù)的整個作用域有效了。標(biāo)識符有效性正是導(dǎo)致函數(shù)語句與函數(shù)表達(dá)式不同的關(guān)鍵所在(下一節(jié)將會展示命名函數(shù)表達(dá)式的具體行為)。/ 此時,foo還沒有typeof foo; / undefined if (true) / 一進(jìn)入這個塊,foo就被并在整個作用域中有效了functionfoo() return 1; else

15、 / 永遠(yuǎn)function進(jìn)入這個塊,因此這里的foo永遠(yuǎn)foo() return 2; 被typeof foo; / function通常,可以通過下面這樣符合標(biāo)準(zhǔn)(但更繁瑣一點)的代碼來模擬前例中函數(shù)語句的行為:var foo; if (true) foo = function foo() return 1; ;else foo = function foo() return 2; ;4.函數(shù)語句與函數(shù)或命名函數(shù)表達(dá)式的字符串表示類似(而且包含標(biāo)識符即此例中的foo):if (true) function foo() return 1; String(foo); / function fo

16、o() return 1; 5.最后,早期基于Gecko的實現(xiàn)(Firefox 3及以前版本)中存在一個bug,即函數(shù)語句覆蓋函數(shù)的方式不正確。在這些早期的實現(xiàn)中,函數(shù)語句不知何故不能覆蓋函數(shù):/ 函數(shù)function foo() return 1; if (true) / 使用函數(shù)語句來重寫function foo() return 2; foo(); / FF及以前版本返回1,F(xiàn)F3.5及以后版本返回2/ 但是,如果前面是函數(shù)表達(dá)式,則沒有這個問題var foo = function() return 1; ; if (true) function foo() return 2; foo(

17、); / 在所有版本中都返回2大家請注意,Safari的某些早期版本(至少包括1.2.3、2.0 - 2.0.4以及3.0.4,可能也包括更早的版本)實現(xiàn)了與SpiderMonkey完全一樣的函數(shù)語句。本節(jié)所有的例子(不包括最后一個bug示例),在Safari的那些版本中都會得到與Firefox完全相同的結(jié)果。此外,Blackberry(至少包括8230、9000和9530)瀏覽器好像也具有類似的行為。上述這種行為的差異化再次說明千萬不能盲目地依賴這些擴(kuò)展啊(如下所述,可以根據(jù)特性測試來使用函數(shù)表達(dá)式。譯者注)!命名函數(shù)表達(dá)式函數(shù)表達(dá)式實際上還是很常見的。Web開發(fā)中有一個常用的模式,即基于對

18、某種特性的測試來“”函數(shù)定義,從而實現(xiàn)性能最優(yōu)化。由于這種通常都出現(xiàn)在相同的作用域中,因此基本上一定要使用函數(shù)表達(dá)式。畢竟,如前所述,不應(yīng)該根據(jù)條件來執(zhí)行函數(shù):/ 這里的contains取自APE Javascript庫的源代碼, var contains = (function() var docEl = document.documentElement;為,作者蓋瑞特斯密特if (typeof docEpareDocumentPosition != return function(el, b) return (pareDocumentPosition(b) &undefined) 16)

19、!= 0;else if (typeof docEl.contains != undefined) return function(el, b) return el != b & el.contains(b);return function(el, b) if (el = b) return while (el != b & (b return el = b;)();false;= b.parentNode) != null);提到命名函數(shù)表達(dá)式,很顯然,指的就是有名字(技術(shù)上稱為標(biāo)識符)的函數(shù)表達(dá)式。在最前面的例子中,var bar = function foo();實際上就是一個以foo作

20、為函數(shù)名字的函數(shù)表達(dá)式。對此,有一個細(xì)節(jié)特別重要,請大家一定要記住,即這個名字只在新定義的函數(shù)的作用域中有效規(guī)范要求標(biāo)識符不能在的作用域中有效:var f = function foo()return typeof foo; / foo只在內(nèi)部作用域中有效;/ foo在“外部”永遠(yuǎn)是不可見的typeof foo; / undefined f(); / function那么,這些所謂名函數(shù)表達(dá)式到底有什么用呢?為什么還要給它們起個名字呢?就是有名字的函數(shù)可以讓調(diào)試過程更加方便。在調(diào)試應(yīng)用程序時,如果調(diào)用棧中的項都有各自描述性的名字,那么調(diào)試過程帶給人的就是另一種完全不同的感受。調(diào)試器中的函數(shù)名在

21、函數(shù)有相應(yīng)標(biāo)識符的情況下,調(diào)試器會將該標(biāo)識符作為函數(shù)的名字顯示在調(diào)用棧中。有的調(diào)試器(例如Firebug)甚至?xí)槟涿瘮?shù)起個名字并則,而依據(jù)簡單的,讓它們與那些函數(shù)的變量具有相同的??蛇z憾的是,這些調(diào)試器通常只使用簡單的規(guī)規(guī)則提取出來的“名字”有時候沒有多大價值,甚至?xí)玫藉e誤結(jié)果。(Such extraction is usually quitefragile and often produces false results. )下面我們來看一個簡單的例子:function returnfunction returnfunctionfoo()bar();bar()baz();baz()de

22、bugger;foo();/這里使用函數(shù)定義了3個函數(shù)當(dāng)調(diào)試器停止在debugger語句時,F(xiàn)irgbug的調(diào)用??雌饋矸浅G逦篵az bar fooexpr_test.html()這樣,我們就知道foo調(diào)用了bar,而后者接著又調(diào)用了baz(而foo本身又在expr_test.html文檔的全局作用域中被調(diào)用)。但真正值得稱道的,則是Firebug會在我們使用表達(dá)式的情況下,替我們函數(shù)的“名字”:function foo() return bar();var bar = function() return baz();function baz() debugger;foo();/ 調(diào)用棧:

23、baz bar() fooexpr_test.html()相反,不那么令人滿意的情況是,當(dāng)函數(shù)表達(dá)式復(fù)雜一些時(現(xiàn)實中差不多總是如此),調(diào)試器再如何盡力也用。結(jié)果,我們只能在調(diào)用棧中顯示函數(shù)名字的位置上赫然看到一個問號:起多大的作function foo() return bar();var bar = (function()if (window.addEventListener) return function()return baz();else if (window.attachEvent) return function() return baz();)();function baz(

24、) debugger;foo();/ 調(diào)用棧:baz (?)()fooexpr_test.html()此外,當(dāng)把一個函數(shù)賦值給多個變量時,還會出現(xiàn)一個令人困惑的問題:function foo()return baz();var bar = function() debugger;var baz = bar;bar = function() alert(spoofed);foo();/ 調(diào)用棧:bar() fooexpr_test.html()可見,調(diào)用棧中顯示的是foo調(diào)用了bar。但實際情況顯然并非如此。之所以會造成這種困惑,完全是因為baz與另一個函數(shù)包含代碼alert(spoofed);

25、的函數(shù)“交換了”單的大多數(shù)情況而言就沒有什么用處了。所致。實事求是地說,這種方式在簡單的情況下固然好,但對于不那么簡歸根結(jié)底,只有命名函數(shù)表達(dá)式才是產(chǎn)生可靠的棧調(diào)用信息的唯一途徑。下面我們有意使用命名函數(shù)表達(dá)式來重寫前面的 例子。請大家注意,從自執(zhí)行包裝塊中返回的兩個函數(shù)都被命名為了bar:function foo() return bar();var bar = (function()if (window.addEventListener) return function bar()return baz();else if (window.attachEvent) return functi

26、on bar() return baz();)();function baz() debugger;foo();/ 這樣,我們就又可以看到清晰的調(diào)用棧信息了!baz bar fooexpr_test.html()在我們?yōu)榘l(fā)現(xiàn)這根救命稻草而歡呼雀躍之前,請大家稍安勿躁,再聽我聊一聊大家所衷愛的JScript。JScript的bug令人討厭的是,JScript(也就是IE的ECMAScript實現(xiàn))嚴(yán)重了命名函數(shù)表達(dá)式。JScript搞得現(xiàn)如今很多人都站出來命名函數(shù)表達(dá)式。而且,直到JScript的最近一版IE8中使用的5.8版仍然存在下列的所有怪異問題。下面我們就來看看IE在它的這個“破”實現(xiàn)中

27、到底都搞出了哪些花樣。唉,只有知已知彼,才能百戰(zhàn)不殆嘛。請注意,為了清晰起見,我會通過一個個相對的小例子來說明這些問題,雖然這些問題很可能是一個主bug引起的一連串的后果。例1:函數(shù)表達(dá)式的標(biāo)識符滲透到外部(enclosing)作用域中var f = function g();typeof g; / function還有人記得嗎,我們說過:命名函數(shù)表達(dá)式的標(biāo)識符在其外部作用域中是無效的? 好啦,JScript明目張膽地了這一規(guī)定上面例子中的標(biāo)識符g被為函數(shù)對象。這是最讓人頭疼的一個問題了。這樣,任何標(biāo)識符都可能會在不經(jīng)意間“污染”某個外部作用域甚至是全局作用域。而且,這種污染常常就是那些難以捕

28、獲的bug的來源。例2:將命名函數(shù)表達(dá)式同時當(dāng)作函數(shù)和函數(shù)表達(dá)式typeof g; / functionvar f = function g();如前所述,在特定的執(zhí)行環(huán)境中,函數(shù)會先于任何表達(dá)式被。上面這個例子展示了JScript實際上是把命名函數(shù)表達(dá)式當(dāng)作函數(shù)了;因為它在“實際的”之前就了g。這個例子進(jìn)而引出了下一個例子:例3:命名函數(shù)表達(dá)式會創(chuàng)建兩個截然不同的函數(shù)對象!var f = function g();f = g; / falsef.expando = foo;g.expando; / undefined問題至此就比較嚴(yán)重了。或者可以說修改其中一個對象對另一個絲毫沒有影響這簡直就

29、是胡鬧!通過例子可以看出,出現(xiàn)兩個不同的對象會存在什么風(fēng)險。假如你想利用緩存機(jī)制,在f的屬性中保存某個信息,然后又想當(dāng)然地認(rèn)為可以通過 象的g的同名屬性取得該信息,那么你的麻煩可就大了。相同對再來看一個稍微復(fù)雜點的情況。例4:順序地函數(shù)而忽略條件語句塊var f = function g() return 1;if (false) f = function g() return 2;g(); / 2要查找這個例子中的bug就要一些了。但導(dǎo)致bug的,而由于JScript中的函卻非常簡單。首先,g被當(dāng)作函數(shù)數(shù)不受條件代碼塊約束(與條件代碼塊無關(guān)),所以在“該死的”if分支中,g被當(dāng)作另一個函數(shù)f

30、unction g() return 2又被了一次。然后,所有“常規(guī)的”表達(dá)式被求值,而此時f被賦予了另一個新創(chuàng)建的對象的。由于在對表達(dá)式求值的時候,永遠(yuǎn)進(jìn)入“該死的”if分支,因此f就會繼續(xù)第一個函數(shù)function g() return 1 。分析到這里,問題就很清楚了:假如你不夠細(xì)心,在f中調(diào)用了g(在執(zhí)行遞歸操作的時候會這樣做。譯者注),那么實際上將會調(diào)用一個毫不相干 的g函數(shù)對象(即返回2的那個函數(shù)對象。譯者注)。聰明的讀者可能會聯(lián)想到:在將不同的函數(shù)對象與arguments.callee進(jìn)行比較時,這個問題會有所表現(xiàn)嗎?callee到底是 還是g呢?下面我們就來看一看:fvar f

31、 = function g() return arguments.callee = f, arguments.callee = g;f(); / true, falseg(); / false, true看到了吧,arguments.callee的始終是被調(diào)用的函數(shù)。實際上,這應(yīng)該是件好事兒,你一會兒就知道了。另一個“意外行為”的好玩的例子,當(dāng)我們在不包含的賦值語句中使用命名函數(shù)表達(dá)式時可以看到。不過,此時函數(shù)的名字必須與它的標(biāo)識符相同才行:(function()f = function f();)();眾所周知(但愿如此。譯者注),不包含的賦值語句(注意,我們不建議使用,這里只是出于示范需要

32、才用的)在這里會創(chuàng)建一個全局屬性f。而這也是標(biāo)準(zhǔn)實現(xiàn)的行為。,JScript的bug在這里又會出點亂子。由于JScript把命名函數(shù)表達(dá)式來(參見前面的“例2”當(dāng)作函數(shù)(#example_2_named_function_expression_is_treated_as_both_function_declaration_and_function_expression)),因此在變?yōu)榫植孔兞?。然后,在函?shù)執(zhí)行時,賦值語句已經(jīng)不是未的了(因為f已經(jīng)被量階段,f會被為局部變量了。存在。譯者注),右手邊的function f()就會被直接賦給剛剛創(chuàng)建的局部變量f。而全局作用域中的f根本看完這個例子后

33、,相信大家就會明白,如果你對JScript的“怪異”行為缺乏了解,你的代碼中出現(xiàn)“嚴(yán)重不符合預(yù)期”的行為就不難理解了。明白了JScript的缺陷以后,要采取哪些預(yù)防措施就非常清楚了。首先,要注意防范標(biāo)識符泄漏( 滲透)(不讓標(biāo)識符污染外部作用域)。其次,應(yīng)該永遠(yuǎn)不被用作函數(shù)名稱的標(biāo)識符;還記得前面例子中那個厭的標(biāo)識符g嗎?如果我們能夠當(dāng)g不存在,可以避免多少不必要的麻煩哪。因此,關(guān)鍵就在于始終要通過f或者arguments.callee來函數(shù)。如果你使用了命名函數(shù)表達(dá)式,那么應(yīng)該只在調(diào)試的時候利用那個名字。最后,還要記住一點,一定要把NFE( Named FunciontExpresssion

34、s, 命名函數(shù)表達(dá)式)期間錯誤創(chuàng)建的函數(shù)干凈。嗯,對于上面最后一點,我覺得還要再啰嗦兩句:JScript的內(nèi)存管理熟悉上述JScript缺陷之后,再使用這些有毛病的結(jié)構(gòu),就會發(fā)現(xiàn)內(nèi)存占用方面的潛在問題。下面看一個簡單的例子:var f = (function() if (true) return function g();return function g();)();我們知道,這里(函數(shù))調(diào)用返回的函數(shù)帶有標(biāo)識符g的函數(shù)被賦值給了外部的f。我們也知道,命名函數(shù)表達(dá)式會導(dǎo)致產(chǎn)生多余的函數(shù)對象,而該對象與返回的函數(shù)對象不是一回事。由于有一個多余的g函數(shù)被“截留”在了返回函數(shù)的閉包中,因此內(nèi)存問題

35、就出現(xiàn)了。這是因為(if語句)內(nèi)部(的)函數(shù)與討厭的g是在同一個作用域中被的。在這種情況下 ,除非我們顯式地斷開對(調(diào)用返回的) g函數(shù)的,否則那個討厭的家伙會一直占著內(nèi)存不放。var f = (function() var f, g;if (true) f = function g();else f = function g();/ 廢掉g,這樣它就g = null; return f;)();再多余的函數(shù)了請注意,這里也明確了。通過廢掉對g的了變量g,因此賦值語句g = null就在符合標(biāo)準(zhǔn)的客戶端(如非JScript實現(xiàn))中創(chuàng)建全局變量g,收集器就可以把g的那個隱式創(chuàng)建的函數(shù)對象清除了。

36、在解決JScript NFE內(nèi)存泄漏問題的過程中,我運(yùn)行了一系列簡單的測試,以便確定廢掉g能夠內(nèi)存。測試這里的測試很簡單。就是通過命名函數(shù)表達(dá)式創(chuàng)建10000個函數(shù),把它們保存在一個數(shù)組中。過一會兒,看看這些函數(shù)到底占用了多少內(nèi)存。然后,再廢掉這些并重復(fù)這一過程。下面是我使用的一個測試用例:function createFn() return (function()var f;if (true) f = function F() return standard;else if (false) f = function F() return alternative;else f = functi

37、on F() return fallback;/ var F = null; return f;)();var arr = ;for (var i=0; i 20.3K7.6K - 18KIE7:without null:with null:14K - 29.7K14K - 27K這個結(jié)果大致驗證了想法顯式地清除多余的確實可以內(nèi)存,但的內(nèi)存空間相對不多。在創(chuàng)建10000個函數(shù)對象的情況下,大約有3MB左右。對于大型應(yīng)用程序,以及需要長時間運(yùn)行或者在低內(nèi)存設(shè)備(如手持設(shè)備)上運(yùn)行的程序而言,這是絕對需要考慮的。但對小型而言,這點差別可能也算不了什么。有讀者可能認(rèn)文到此差不多就該結(jié)尾了實際上還差得

38、遠(yuǎn)呢 :)。我還想再多談一點,這些內(nèi)容涉及的是Safari 2.x。Safari中存在的bug在Safari較早的版本Safari 2.x系列中,也存在一些鮮為人知的與NFE有關(guān)的bug。我在Web上看到有人說Safari 2.x不支持NFE () 。實際上不是那么回事。Safari確實支持NFE,只不過它的實現(xiàn)中存在bug而已(很快你就會看到)。在某些情況下,Safari 2.x遇到函數(shù)表達(dá)式時會出現(xiàn)不能完全SyntaxError),只會“默默地知難而退”:程序的問題。而且,此時的Safari拋出任何錯誤(例如(function f()(); / = NFEalert(1); / 由于前面的

39、表達(dá)式破壞了整個程序,因此這一行永遠(yuǎn)執(zhí)行經(jīng)過多次測試,我得出一個結(jié)論:Safari 2.x 不能非賦值表達(dá)式中名函數(shù)表達(dá)式。下面是一些賦值表達(dá)式的例子:/ 變量var f = 1;/ 簡單賦值f = 2, g = 3;/ 返回語句(function() return (f = 2);)();換句話說,把命名函數(shù)表達(dá)式放到一個賦值表達(dá)式中會讓Safari“很高興”:(function f(); / 失敗var f = function f(); / 沒問題(function()return function f(); / 失敗)();(function()return (f = function

40、 f(); / 沒問題)();setTimeout(function f() , 100); / 失敗.prototype = say: function say() . / 失敗.prototype.say = function say() . ; / 沒問題同時這也就意味著,在不使用賦值表達(dá)式的情況下,我們不能使用習(xí)以為常的模式返回命名函數(shù)表達(dá)式:/ 以下返回命名函數(shù)表達(dá)式的常見模式,對Safari 2.x來說是不兼容的: (function()if (featureTest) return function f();return function f();)();/ 在Safari 2.

41、x中,應(yīng)該使用以下稍麻煩一點的方式: (function()var f;if (featureTest) f = function f();else f = function f();return f;)();/ 或者,像下面這樣也行:(function() var f;if (featureTest) return (f = function f();return (f = function f();)();/*,這樣一來,就額外使用了一個對函數(shù)的,而該還被封閉在了返回函數(shù)的閉包中。為了最大限度地降低額外的內(nèi)存占用,可以考慮把所有命名函數(shù)表達(dá)式都賦值給一個變量。*/var temp;(fun

42、ction()if (featureTest) return ( tempreturn ( temp =)();= function f();function f();.(function()if (featureTest2) return ( temp = function g();return ( temp = function g();)();/*這樣,后續(xù)的賦值語句通過“重用”前面的*/,達(dá)到了不過多占用內(nèi)存的目的。如果兼容Safari 2.x非常重要,就應(yīng)該保證源代碼中不能出現(xiàn)任何“不兼容”的結(jié)構(gòu)。雖然這樣做不免會讓人著急上火,可只要抓住了問題的根源,還是絕對能夠做到的。對了,還有個

43、小問題必須說明一下:在Safari 2.x中命名函數(shù)時,函數(shù)的字符串表示包含函數(shù)的標(biāo)識符:var f = function g();/ 看到了嗎,函數(shù)的字符串表示中沒有標(biāo)識符gString(f); / function () 這不算什么大問題。但正如我以前說過的,函數(shù)的反編譯結(jié)果是無論如何也不能相信的 (/prototype/those-tricky-functions/)。SpiderMonkey的怪癖大家都知道,命名函數(shù)表達(dá)式的標(biāo)識符只在函數(shù)的局部作用域中有效。但包含這個標(biāo)識符的局部作用域又是什么樣子的嗎?其實 非常簡單。在命名函數(shù)表達(dá)式被求值時,會創(chuàng)建一個特殊的對象,該對象的唯一目的就是

44、保存一個屬性,而這個屬性的名字對 應(yīng)著函數(shù)標(biāo)識符,屬性的值對應(yīng)著那個函數(shù)。這個對象會被注入到當(dāng)前作用域鏈的前端。然后,被“擴(kuò)展”的作用域鏈又被用于初始化函數(shù)。在這里(想象一下本山大叔在小品火炬手中獲獎感言的情景吧。譯者注),有一點十分有意思,那就是ECMA-262 定義這個(保存函數(shù)標(biāo)識符的)“特殊”對象的方式。標(biāo)準(zhǔn)說“像調(diào)用new Object()表達(dá)式那樣”創(chuàng)建這個對象。如果從字面上來理解這句話,那么這個對象就應(yīng)該是全局Object的一個實例。然而,只有一個實現(xiàn)是按照標(biāo)準(zhǔn)字面上的要求這么做的,這個實現(xiàn) 就是SpiderMonkey。因此,在SpiderMonkey中,擴(kuò)展Object.pr

45、ototype有可能會干擾函數(shù)的局部作用域:Ototype.x = outer;(function()var x = inner;/*函數(shù)foo的作用域鏈中有一個特殊的對象用于保存函數(shù)的標(biāo)識符。這個特殊的對象實際上就是 foo: function ob當(dāng)通過作用域鏈x時,首先的是foo的局部環(huán)境。如果沒有找到x,則繼續(xù)搜索作用域鏈中的下一個對象。下一個就是保存函數(shù)標(biāo)識符的那個對象 foo: ,由于該對象繼承自O(shè)totype,所以在而這個x的值也就是Ototype.x的值(outer)。結(jié)果,外部函數(shù)的作用域(包含x = inner的作用域)

46、*/(function foo()alert(x); / 提示框中顯示:outer)();)();不過,更高版本的SpiderMonkey改變了上述行為,可能是認(rèn)為那是一個安全漏洞。也就是說,“特殊”對象不再繼承Ototype了。不過,如果你使用Firefox 3或者更低版本,還可以“重溫”這種行為。另一個把內(nèi)部對象實現(xiàn)為全局Object對象的是黑莓( Blackberry) 瀏覽器。目前,它的活動對象(Activation Object)仍然繼承Ototype。,ECMA-262并沒有說活動對象也要“像調(diào)用new Object()表達(dá)式那樣”來創(chuàng)建(或者說

47、像創(chuàng)建保存NFE標(biāo)識符的對象一樣創(chuàng)建)。 人家規(guī)范只說了活動對象是規(guī)范中的一種機(jī)制。好,那我們下面就來看看黑莓瀏覽器的行為吧:Ototype.x = outer;(function()var x = inner;(function()/*在沿著作用域鏈x的過程中,首先會搜索局部函數(shù)的活動對象。當(dāng)然,在該對象中找不到x。,由于活動對象繼承自O(shè)totype,因此搜索x的下一個目標(biāo)就是Ototype;而Ototype中又確實有x的定義。結(jié)果,x的值就被為outer。跟前面的例子差不多,包含x = inner的外部函數(shù)的作用域(活

48、動對象)就*/被了。alert(x); / 提示框中顯示:outer)();)();雖然這有點讓人不可思議,但更令人匪夷所思的則是函數(shù)中的變量甚至?xí)c已有的Ototype的成員發(fā)生:(function()var constructor = function() return 1; ;(function()constructor(); / 求值結(jié)果是(即相當(dāng)于調(diào)用了Ototype.constructor()。譯者注)而不是1constructor = Ototype.constructor; /truetoString = Object.pr

49、ototype.toString;/true/ )();)();解決方案var fn = (function()/要var f;函數(shù)的變量/ if有條件地創(chuàng)建命名函數(shù)并將其賦值給f (true) f = function F()else if (false) f = function F()else f = function F()/一個與函數(shù)名(標(biāo)識符)對應(yīng)的變量,并賦值為null這實際上是給相應(yīng)標(biāo)識符的函數(shù)對象作了一個標(biāo)記,以便回收器知道可以回收它了var F = null;/ 返回根據(jù)條件定義的函數(shù)return f;)();最后,我要給出一個應(yīng)用上述“技術(shù)”的實例。這是一個跨瀏覽器的ad

50、dEvent函數(shù)的代碼:/ 1) 使用的作用域包含var addEvent = (function()var docEl = document.documentElement;/ 2)要函數(shù)的變量var fn;if (docEl.addEventListener) / 3) 有意給函數(shù)一個描述性的標(biāo)識符fn = function addEvent(element, eventName, callback) element.addEventListener(eventName, callback, false);else if (docEl.attachEvent) fn = function

51、addEvent(element, eventName, callback) element.attachEvent(on + eventName, callback);else fn = function addEvent(element, eventName, callback) elementon + eventName = callback;/ var4)清除由JScript創(chuàng)建的addEvent函數(shù)一定要保證在賦值前使用var關(guān)鍵字除非函數(shù)頂部已經(jīng)addEvent = null;了addEvent/ 5) 最后返回由fn return fn;)();的函數(shù)替代方案不要忘了,如果我們不想在調(diào)用棧中保留描述性的名字,實際上還有其他選擇。換句話說,就是還存在不必使用命名函數(shù)表達(dá)式的方案。首先,很多時候都可以通過而非表達(dá)式定義函數(shù)。這個方案只適合不需要創(chuàng)建多個函數(shù)的情形:var hasClassName = (function()/ 定義私有變量var cache = ;/ 使用函數(shù)function hasClas

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論