亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

命名函數(shù)表達(dá)式探秘

作者:Juriy "kangax" Zaytsev
譯者:為之漫筆
發(fā)表時(shí)間:2009年6月17日 最近修改:2009年10月9日
翻譯時(shí)間:2009年12月9日 修訂時(shí)間:2009年12月22日
  1. 前言
  2. 函數(shù)表達(dá)式與函數(shù)聲明
  3. 函數(shù)語句
  4. 命名函數(shù)表達(dá)式
  5. 調(diào)試器中的函數(shù)名
  6. JScript的bug
  7. JScript的內(nèi)存管理
  8. 測(cè)試
  9. Safari中存在的bug
  10. SpiderMonkey的怪癖
  11. 解決方案
  12. 替代方案
  13. WebKit的displayName
  14. 對(duì)未來的思考
  15. 致謝

前言

我覺得很奇怪,網(wǎng)上好像一直沒有人認(rèn)真地討論過命名函數(shù)表達(dá)式(Named Function Expression,即“有名字函數(shù)表達(dá)式”,與“匿名函數(shù)”相對(duì)?!g者注)。而這也許正是各種各樣的誤解隨處可見的一個(gè)原因。在這篇文章里,我打算從理論和實(shí)踐兩個(gè)方面出發(fā),對(duì)這些令人驚嘆的JavaScript結(jié)構(gòu)的優(yōu)缺點(diǎn)給出一個(gè)結(jié)論。

簡(jiǎn)單來講,命名函數(shù)表達(dá)式只有一個(gè)用處——在調(diào)試器或性能分析程序中描述函數(shù)的名稱。沒錯(cuò),也可以使用函數(shù)名實(shí)現(xiàn)遞歸,但你很快就會(huì)知道,目前來看這通常是不切實(shí)際的。當(dāng)然,如果你不關(guān)注調(diào)試,那就沒什么可擔(dān)心的。否則,就應(yīng)該往下看一看,看看在跨瀏覽器開發(fā)中都會(huì)出現(xiàn)哪些小毛病(glitch),也看看應(yīng)該怎樣解決它們。

一開始呢,我會(huì)先介紹一下什么是函數(shù)表達(dá)式,以及現(xiàn)代調(diào)試器如何處理它們之類的內(nèi)容。要是你比較心急,請(qǐng)直接跳到“最終方案”部分,該部分詳細(xì)說明了怎樣才能安全地使用這些結(jié)構(gòu)。

函數(shù)表達(dá)式與函數(shù)聲明

在ECMAScript中,有兩個(gè)最常用的創(chuàng)建函數(shù)對(duì)象的方法,即使用函數(shù)表達(dá)式或者使用函數(shù)聲明。這兩種方法之間的區(qū)別可謂 相當(dāng)?shù)亓钊死Щ?/strong>;至少我是相當(dāng)?shù)乩Щ蟆?duì)此,ECMA規(guī)范只明確了一點(diǎn),即函數(shù)聲明 必須始終帶有一個(gè)標(biāo)識(shí)符(Identifier)——也就是函數(shù)名唄,而函數(shù)表達(dá)式 則可省略這個(gè)標(biāo)識(shí)符:

函數(shù)聲明:
function Identifier ( FormalParameterList opt ){ FunctionBody }

函數(shù)表達(dá)式:
function Identifier opt ( FormalParameterList opt ){ FunctionBody }

顯然,在省略標(biāo)識(shí)符的情況下, “表達(dá)式” 也就只能是表達(dá)式了??梢遣皇÷詷?biāo)識(shí)符呢?誰知道它是一個(gè)函數(shù)聲明,還是一個(gè)函數(shù)表達(dá)式——畢竟,這種情況下二者是完全一樣的啊?實(shí)踐表明,ECMAScript是通過上下文來區(qū)分這兩者的:假如 function foo(){} 是一個(gè)賦值表達(dá)式的一部分,則認(rèn)為它是一個(gè)函數(shù)表達(dá)式。而如果 function foo(){} 被包含在一個(gè)函數(shù)體內(nèi),或者位于程序(的最上層)中,則將它作為一個(gè)函數(shù)聲明來解析。

  function foo(){}; // 聲明,因?yàn)樗?em>程序的一部分
  var bar = function foo(){}; // 表達(dá)式,因?yàn)樗?em>賦值表達(dá)式(AssignmentExpression)的一部分

  new function bar(){}; // 表達(dá)式,因?yàn)樗?em>New表達(dá)式(NewExpression)的一部分

  (function(){
    function bar(){}; // 聲明,因?yàn)樗?em>函數(shù)體(FunctionBody)的一部分
  })();

還有一種不那么顯而易見的函數(shù)表達(dá)式,就是被包含在一對(duì)圓括號(hào)中的函數(shù)—— (function foo(){})。將這種形式看成表達(dá)式同樣是因?yàn)樯舷挛牡年P(guān)系:(和)構(gòu)成一個(gè)分組操作符,而分組操作符只能包含表達(dá)式:

下面再多看幾個(gè)例子吧:

  
  function foo(){}; // 函數(shù)聲明
  (function foo(){}); // 函數(shù)表達(dá)式:注意它被包含在分組操作符中
  
  try {
    (var x = 5); // 分組操作符只能包含表達(dá)式,不能包含語句(這里的var就是語句)
  } catch(err) {
    // SyntaxError(因?yàn)椤皏ar x = 5”是一個(gè)語句,而不是表達(dá)式——對(duì)表達(dá)式求值必須返回值,但對(duì)語句求值則未必返回值。——譯者注)
  }

不知道大家有沒有印象,在使用 eval 對(duì)JSON求值的時(shí)候,JSON字符串通常是被包含在一對(duì)圓括號(hào)中的—— eval('(' + json + ')')。這樣做的原因當(dāng)然也不例外——分組操作符,也就是那對(duì)圓括號(hào),會(huì)導(dǎo)致解析器強(qiáng)制將JSON的花括號(hào)當(dāng)成表達(dá)式而不代碼塊來解析:

  try {
    { "x": 5 }; // {和}會(huì)被作為塊來解析
  } catch(err) {
    // SyntaxError(“'x':5”只是構(gòu)建對(duì)象字面量的語法,但該語法不能出現(xiàn)在外部的語句塊中?!g者注)
  }
  
  ({ "x": 5 }); // 分組操作符會(huì)導(dǎo)致解析器強(qiáng)制將{和}作為對(duì)象字面量來解析

聲明和表達(dá)式的行為存在著十分微妙而又十分重要的差別。

首先,函數(shù)聲明會(huì)在任何表達(dá)式被解析和求值之前先行被解析和求值。即使聲明位于源代碼中的最后一行,它也會(huì)先于同一作用域中位于最前面的表達(dá)式被求值。還是看個(gè)例子更容易理解。在下面這個(gè)例子中,函數(shù) fn 是在 alert 后面聲明的。但是,在 alert 執(zhí)行的時(shí)候,fn已經(jīng)有定義了:

  alert(fn());

  function fn() {
    return 'Hello world!';
  }

函數(shù)聲明還有另外一個(gè)重要的特點(diǎn),即通過條件語句控制函數(shù)聲明的行為并未標(biāo)準(zhǔn)化,因此不同環(huán)境下可能會(huì)得到不同的結(jié)果。有鑒于此,奉勸大家千萬不要在條件語句中使用函數(shù)聲明,而要使用函數(shù)表達(dá)式。

  // 千萬不要這樣做!
  // 有的瀏覽器會(huì)把foo聲明為返回first的那個(gè)函數(shù)
  // 而有的瀏覽器則會(huì)讓foo返回second

  if (true) {
    function foo() {
      return 'first';
    }
  }
  else {
    function foo() {
      return 'second';
    }
  }
  foo();

  // 記住,這種情況下要使用函數(shù)表達(dá)式:
  var foo;
  if (true) {
    foo = function() {
      return 'first';
    };
  }
  else {
    foo = function() {
      return 'second';
    };
  }
  foo();

想知道使用函數(shù)聲明的實(shí)際規(guī)則到底是什么?繼續(xù)往下看吧。嗯,有人不想知道?那請(qǐng)?zhí)^下面這段摘錄的文字。

FunctionDeclaration(函數(shù)聲明)只能出現(xiàn)在Program(程序)FunctionBody(函數(shù)體)內(nèi)。從句法上講,它們 不能出現(xiàn)在Block(塊){ ... })中,例如不能出現(xiàn)在 ifwhilefor 語句中。因?yàn)?Block(塊) 中只能包含Statement(語句), 而不能包含FunctionDeclaration(函數(shù)聲明)這樣的SourceElement(源元素)。另一方面,仔細(xì)看一看產(chǎn)生規(guī)則也會(huì)發(fā)現(xiàn),唯一可能讓Expression(表達(dá)式)出現(xiàn)在Block(塊)中情形,就是讓它作為ExpressionStatement(表達(dá)式語句)的一部分。但是,規(guī)范明確規(guī)定了ExpressionStatement(表達(dá)式語句)不能以關(guān)鍵字function開頭。而這實(shí)際上就是說,FunctionExpression(函數(shù)表達(dá)式)同樣也不能出現(xiàn)在Statement(語句)Block(塊)中(別忘了Block(塊)就是由Statement(語句)構(gòu)成的)。

由于存在上述限制,只要函數(shù)出現(xiàn)在塊中(像上面例子中那樣),實(shí)際上就應(yīng)該將其看作一個(gè)語法錯(cuò)誤,而不是什么函數(shù)聲明或表達(dá)式。但問題是,我還沒見過哪個(gè)實(shí)現(xiàn)是按照上述規(guī)則來解析這些函數(shù)的;好像每個(gè)實(shí)現(xiàn)都有自己的一套。

有必要提醒大家一點(diǎn),根據(jù)規(guī)范的描述,實(shí)現(xiàn)可以引入語法擴(kuò)展(見第16部分),只不過任何情況下都不能違反規(guī)定。而目前的諸多客戶端也正是照此辦理的。其中有一些會(huì)把塊中的函數(shù)聲明當(dāng)作一般的函數(shù)聲明來解析——把它們提升到封閉作用域的頂部;另一些則引入了不同的語義并采用了稍復(fù)雜一些的規(guī)則。

函數(shù)語句

在諸如此類的對(duì)ECMAScript的語法擴(kuò)展中,有一項(xiàng)就是函數(shù)語句,基于Gecko的瀏覽器(在Mac OS X平臺(tái)的Firefox 1-3.7a1pre中測(cè)試過)目前都實(shí)現(xiàn)了該項(xiàng)擴(kuò)展??墒遣恢罏槭裁矗芏嗳撕孟穸疾恢肋@項(xiàng)擴(kuò)展,也就更談不上對(duì)其優(yōu)劣的評(píng)價(jià)了(MDC(Mozilla Developer Center,Mozilla開發(fā)者中心)提到過這個(gè)問題,但是只有那么三言兩語)。請(qǐng)大家記住,我們是抱著學(xué)習(xí)和滿足自己好奇心的態(tài)度來討論函數(shù)語句的。因此,除非你只針對(duì)基于Gecko的環(huán)境編寫腳本,否則我不建議你使用這個(gè)擴(kuò)展。

閑話少說,下面我們就來看看這些非標(biāo)準(zhǔn)的結(jié)構(gòu)有哪些特點(diǎn):

  1. 一般語句可以出現(xiàn)的地方,函數(shù)語句也可以出現(xiàn)。當(dāng)然包括中:
  2.   if (true) {
        function f(){ }
      }
      else {
        function f(){ }
      }
    
  3. 函數(shù)語句可以像其他語句一樣被解析,包含基于條件執(zhí)行的情形:
  4.   if (true) {
        function foo(){ return 1; }
      }
      else {
        function foo(){ return 2; }
      }
      foo(); // 1
      // 注意其他類型的客戶端會(huì)把這里的foo解析為函數(shù)聲明
      // 因此,第二個(gè)foo會(huì)覆蓋第一個(gè),結(jié)果返回2而不返回1
    
  5. 函數(shù)語句不是在變量初始化期間聲明的,而是在運(yùn)行時(shí)聲明的——與函數(shù)表達(dá)式一樣。不過,一旦聲明,函數(shù)語句的標(biāo)識(shí)符就在函數(shù)的整個(gè)作用域有效了。標(biāo)識(shí)符有效性正是導(dǎo)致函數(shù)語句與函數(shù)表達(dá)式不同的關(guān)鍵所在(下一節(jié)將會(huì)展示命名函數(shù)表達(dá)式的具體行為)。
      // 此時(shí),foo還沒有聲明
      typeof foo; // "undefined"
      if (true) {
        // 一進(jìn)入這個(gè)塊,foo就被聲明并在整個(gè)作用域中有效了
        function foo(){ return 1; }
      }
      else {
        // 永遠(yuǎn)不會(huì)進(jìn)入這個(gè)塊,因此這里的foo永遠(yuǎn)不會(huì)被聲明
        function foo(){ return 2; }
      }
      typeof foo; // "function"
      
    
    通常,可以通過下面這樣符合標(biāo)準(zhǔn)(但更繁瑣一點(diǎn))的代碼來模擬前例中函數(shù)語句的行為:
      var foo;
      if (true) {
        foo = function foo(){ return 1; };
      }
      else {
        foo = function foo() { return 2; };
      }
    
  6. 函數(shù)語句與函數(shù)聲明或命名函數(shù)表達(dá)式的字符串表示類似(而且包含標(biāo)識(shí)符——即此例中的foo):
      if (true) {
        function foo(){ return 1; }
      }
      String(foo); // function foo() { return 1; }
    
  7. 最后,早期基于Gecko的實(shí)現(xiàn)(Firefox 3及以前版本)中存在一個(gè)bug,即函數(shù)語句覆蓋函數(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á)式,則沒有這個(gè)問題
      var foo = function(){ return 1; };
      if (true) {
        function foo(){ return 2; }
      }
      foo(); // 在所有版本中都返回2
    

大家請(qǐng)注意,Safari的某些早期版本(至少包括1.2.3、2.0 - 2.0.4以及3.0.4,可能也包括更早的版本)實(shí)現(xiàn)了與SpiderMonkey完全一樣的函數(shù)語句。本節(jié)所有的例子(不包括最后一個(gè)bug示例),在Safari的那些版本中都會(huì)得到與Firefox完全相同的結(jié)果。此外,Blackberry(至少包括8230、9000和9530)瀏覽器好像也具有類似的行為。上述這種行為的差異化再次說明——千萬不能盲目地依賴這些擴(kuò)展?。ㄈ缦滤觯梢愿鶕?jù)特性測(cè)試來使用函數(shù)表達(dá)式。——譯者注)!

命名函數(shù)表達(dá)式

函數(shù)表達(dá)式實(shí)際上還是很常見的。Web開發(fā)中有一個(gè)常用的模式,即基于對(duì)某種特性的測(cè)試來“偽裝”函數(shù)定義,從而實(shí)現(xiàn)性能最優(yōu)化。由于這種偽裝通常都出現(xiàn)在相同的作用域中,因此基本上一定要使用函數(shù)表達(dá)式。畢竟,如前所述,不應(yīng)該根據(jù)條件來執(zhí)行函數(shù)聲明:

  // 這里的contains取自APE Javascript庫(kù)的源代碼,網(wǎng)址為http://dhtmlkitchen.com/ape/,作者蓋瑞特·斯密特(Garrett Smit)
  var contains = (function() {
    var docEl = document.documentElement;

    if (typeof docEl.compareDocumentPosition != 'undefined') {
      return function(el, b) {
        return (el.compareDocumentPosition(b) & 16) !== 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 false;
      while (el != b && (b = b.parentNode) != null);
      return el === b;
    }
  })();

提到命名函數(shù)表達(dá)式,很顯然,指的就是有名字(技術(shù)上稱為標(biāo)識(shí)符)的函數(shù)表達(dá)式。在最前面的例子中,var bar = function foo(){};實(shí)際上就是一個(gè)以foo作為函數(shù)名字的函數(shù)表達(dá)式。對(duì)此,有一個(gè)細(xì)節(jié)特別重要,請(qǐng)大家一定要記住,即這個(gè)名字只在新定義的函數(shù)的作用域中有效——規(guī)范要求標(biāo)識(shí)符不能在外圍的作用域中有效:

  var f = function foo(){
    return typeof foo; // foo只在內(nèi)部作用域中有效
  };
  // foo在“外部”永遠(yuǎn)是不可見的
  typeof foo; // "undefined"
  f(); // "function"

那么,這些所謂的命名函數(shù)表達(dá)式到底有什么用呢?為什么還要給它們起個(gè)名字呢?

原因就是有名字的函數(shù)可以讓調(diào)試過程更加方便。在調(diào)試應(yīng)用程序時(shí),如果調(diào)用棧中的項(xiàng)都有各自描述性的名字,那么調(diào)試過程帶給人的就是另一種完全不同的感受。

調(diào)試器中的函數(shù)名

在函數(shù)有相應(yīng)標(biāo)識(shí)符的情況下,調(diào)試器會(huì)將該標(biāo)識(shí)符作為函數(shù)的名字顯示在調(diào)用棧中。有的調(diào)試器(例如Firebug)甚至?xí)槟涿瘮?shù)起個(gè)名字并顯示出來,讓它們與那些引用函數(shù)的變量具有相同的角色??蛇z憾的是,這些調(diào)試器通常只使用簡(jiǎn)單的解析規(guī)則,而依據(jù)簡(jiǎn)單的解析規(guī)則提取出來的“名字”有時(shí)候沒有多大價(jià)值,甚至?xí)玫藉e(cuò)誤結(jié)果。(Such extraction is usually quite fragile and often produces false results. )

下面我們來看一個(gè)簡(jiǎn)單的例子:

  function foo(){
    return bar();
  }
  function bar(){
    return baz();
  }
  function baz(){
    debugger;
  }
  foo();

  // 這里使用函數(shù)聲明定義了3個(gè)函數(shù)
  // 當(dāng)調(diào)試器停止在debugger語句時(shí),
  // Firgbug的調(diào)用??雌饋矸浅G逦?
  baz
  bar
  foo
  expr_test.html()

這樣,我們就知道foo調(diào)用了bar,而后者接著又調(diào)用了baz(而foo本身又在expr_test.html文檔的全局作用域中被調(diào)用)。但真正值得稱道的,則是Firebug會(huì)在我們使用匿名表達(dá)式的情況下,替我們解析函數(shù)的“名字”:

  function foo(){
    return bar();
  }
  var bar = function(){
    return baz();
  }
  function baz(){
    debugger;
  }
  foo();

  // 調(diào)用棧:
  baz
  bar()
  foo
  expr_test.html()

相反,不那么令人滿意的情況是,當(dāng)函數(shù)表達(dá)式復(fù)雜一些時(shí)(現(xiàn)實(shí)中差不多總是如此),調(diào)試器再如何盡力也不會(huì)起多大的作用。結(jié)果,我們只能在調(diào)用棧中顯示函數(shù)名字的位置上赫然看到一個(gè)問號(hào):

  function foo(){
    return bar();
  }
  var bar = (function(){
    if (window.addEventListener) {
      return function(){
        return baz();
      }
    }
    else if (window.attachEvent) {
      return function() {
        return baz();
      }
    }
  })();
  function baz(){
    debugger;
  }
  foo();

  // 調(diào)用棧:
  baz
  (?)()
  foo
  expr_test.html()

此外,當(dāng)把一個(gè)函數(shù)賦值給多個(gè)變量時(shí),還會(huì)出現(xiàn)一個(gè)令人困惑的問題:

  function foo(){
    return baz();
  }
  var bar = function(){
    debugger;
  };
  var baz = bar;
  bar = function() { 
    alert('spoofed');
  }
  foo();

  // 調(diào)用棧:
  bar()
  foo
  expr_test.html()

可見,調(diào)用棧中顯示的是foo調(diào)用了bar。但實(shí)際情況顯然并非如此。之所以會(huì)造成這種困惑,完全是因?yàn)?code>baz與另一個(gè)函數(shù)——包含代碼alert('spoofed');的函數(shù)——“交換了”引用所致。實(shí)事求是地說,這種解析方式在簡(jiǎn)單的情況下固然好,但對(duì)于不那么簡(jiǎn)單的大多數(shù)情況而言就沒有什么用處了。

歸根結(jié)底,只有命名函數(shù)表達(dá)式才是產(chǎn)生可靠的棧調(diào)用信息的唯一途徑。下面我們有意使用命名函數(shù)表達(dá)式來重寫前面的例子。請(qǐng)大家注意,從自執(zhí)行包裝塊中返回的兩個(gè)函數(shù)都被命名為了bar

  function foo(){
    return bar();
  }
  var bar = (function(){
    if (window.addEventListener) {
      return function bar(){
        return baz();
      }
    }
    else if (window.attachEvent) {
      return function bar() {
        return baz();
      }
    }
  })();
  function baz(){
    debugger;
  }
  foo();

  // 這樣,我們就又可以看到清晰的調(diào)用棧信息了!
  baz
  bar
  foo
  expr_test.html()

在我們?yōu)榘l(fā)現(xiàn)這根救命稻草而歡呼雀躍之前,請(qǐng)大家稍安勿躁,再聽我聊一聊大家所衷愛的JScript。

JScript的bug

令人討厭的是,JScript(也就是IE的ECMAScript實(shí)現(xiàn))嚴(yán)重混淆了命名函數(shù)表達(dá)式。JScript搞得現(xiàn)如今很多人都站出來反對(duì)命名函數(shù)表達(dá)式。而且,直到JScript的最近一版——IE8中使用的5.8版——仍然存在下列的所有怪異問題。

下面我們就來看看IE在它的這個(gè)“破”實(shí)現(xiàn)中到底都搞出了哪些花樣。唉,只有知已知彼,才能百戰(zhàn)不殆嘛。請(qǐng)注意,為了清晰起見,我會(huì)通過一個(gè)個(gè)相對(duì)獨(dú)立的小例子來說明這些問題,雖然這些問題很可能是一個(gè)主bug引起的一連串的后果。

例1:函數(shù)表達(dá)式的標(biāo)識(shí)符滲透到外部(enclosing)作用域中

    var f = function g(){};
    typeof g; // "function"

還有人記得嗎,我們說過:命名函數(shù)表達(dá)式的標(biāo)識(shí)符在其外部作用域中是無效的? 好啦,JScript明目張膽地違反了這一規(guī)定——上面例子中的標(biāo)識(shí)符g被解析為函數(shù)對(duì)象。這是最讓人頭疼的一個(gè)問題了。這樣,任何標(biāo)識(shí)符都可能會(huì)在不經(jīng)意間“污染”某個(gè)外部作用域——甚至是全局作用域。而且,這種污染常常就是那些難以捕獲的bug的來源。

例2:將命名函數(shù)表達(dá)式同時(shí)當(dāng)作函數(shù)聲明和函數(shù)表達(dá)式

    typeof g; // "function"
    var f = function g(){};

如前所述,在特定的執(zhí)行環(huán)境中,函數(shù)聲明會(huì)先于任何表達(dá)式被解析。上面這個(gè)例子展示了JScript實(shí)際上是把命名函數(shù)表達(dá)式當(dāng)作函數(shù)聲明了;因?yàn)樗凇皩?shí)際的”聲明之前就解析了g

這個(gè)例子進(jìn)而引出了下一個(gè)例子:

例3:命名函數(shù)表達(dá)式會(huì)創(chuàng)建兩個(gè)截然不同的函數(shù)對(duì)象!

    var f = function g(){};
    f === g; // false

    f.expando = 'foo';
    g.expando; // undefined

問題至此就比較嚴(yán)重了?;蛘呖梢哉f修改其中一個(gè)對(duì)象對(duì)另一個(gè)絲毫沒有影響——這簡(jiǎn)直就是胡鬧!通過例子可以看出,出現(xiàn)兩個(gè)不同的對(duì)象會(huì)存在什么風(fēng)險(xiǎn)。假如你想利用緩存機(jī)制,在f的屬性中保存某個(gè)信息,然后又想當(dāng)然地認(rèn)為可以通過引用相同對(duì)象的g的同名屬性取得該信息,那么你的麻煩可就大了。

再來看一個(gè)稍微復(fù)雜點(diǎn)的情況。

例4:只管順序地解析函數(shù)聲明而忽略條件語句塊

    var f = function g() {
        return 1;
    };
    if (false) {
        f = function g(){
            return 2;
        };
    }
    g(); // 2

要查找這個(gè)例子中的bug就要困難一些了。但導(dǎo)致bug的原因卻非常簡(jiǎn)單。首先,g被當(dāng)作函數(shù)聲明解析,而由于JScript中的函數(shù)聲明不受條件代碼塊約束(與條件代碼塊無關(guān)),所以在“該死的”if分支中,g被當(dāng)作另一個(gè)函數(shù)——function g(){ return 2 }——又被聲明了一次。然后,所有“常規(guī)的”表達(dá)式被求值,而此時(shí)f被賦予了另一個(gè)新創(chuàng)建的對(duì)象的引用。由于在對(duì)表達(dá)式求值的時(shí)候,永遠(yuǎn)不會(huì)進(jìn)入“該死的”if分支,因此f就會(huì)繼續(xù)引用第一個(gè)函數(shù)——function g(){ return 1 }。分析到這里,問題就很清楚了:假如你不夠細(xì)心,在f中調(diào)用了g(在執(zhí)行遞歸操作的時(shí)候會(huì)這樣做?!g者注),那么實(shí)際上將會(huì)調(diào)用一個(gè)毫不相干的g函數(shù)對(duì)象(即返回2的那個(gè)函數(shù)對(duì)象?!g者注)。

聰明的讀者可能會(huì)聯(lián)想到:在將不同的函數(shù)對(duì)象與arguments.callee進(jìn)行比較時(shí),這個(gè)問題會(huì)有所表現(xiàn)嗎?callee到底是引用f還是引用g呢?下面我們就來看一看:

  var f = function g(){
    return [
      arguments.callee == f,
      arguments.callee == g
    ];
  };
  f(); // [true, false]
  g(); // [false, true]

看到了吧,arguments.callee引用的始終是被調(diào)用的函數(shù)。實(shí)際上,這應(yīng)該是件好事兒,原因你一會(huì)兒就知道了。

另一個(gè)“意外行為”的好玩的例子,當(dāng)我們在不包含聲明的賦值語句中使用命名函數(shù)表達(dá)式時(shí)可以看到。不過,此時(shí)函數(shù)的名字必須與引用它的標(biāo)識(shí)符相同才行:

  (function(){
    f = function f(){};
  })();

眾所周知(但愿如此?!g者注),不包含聲明的賦值語句(注意,我們不建議使用,這里只是出于示范需要才用的)在這里會(huì)創(chuàng)建一個(gè)全局屬性f。而這也是標(biāo)準(zhǔn)實(shí)現(xiàn)的行為。可是,JScript的bug在這里又會(huì)出點(diǎn)亂子。由于JScript把命名函數(shù)表達(dá)式當(dāng)作函數(shù)聲明來解析(參見前面的“例2”),因此在變量聲明階段,f會(huì)被聲明為局部變量。然后,在函數(shù)執(zhí)行時(shí),賦值語句已經(jīng)不是未聲明的了(因?yàn)閒已經(jīng)被聲明為局部變量了。——譯者注),右手邊的function f(){}就會(huì)被直接賦給剛剛創(chuàng)建的局部變量f。而全局作用域中的f根本不會(huì)存在。

看完這個(gè)例子后,相信大家就會(huì)明白,如果你對(duì)JScript的“怪異”行為缺乏了解,你的代碼中出現(xiàn)“嚴(yán)重不符合預(yù)期”的行為就不難理解了。

明白了JScript的缺陷以后,要采取哪些預(yù)防措施就非常清楚了。首先,要注意防范標(biāo)識(shí)符泄漏(滲透)(不讓標(biāo)識(shí)符污染外部作用域)。其次,應(yīng)該永遠(yuǎn)不引用被用作函數(shù)名稱的標(biāo)識(shí)符;還記得前面例子中那個(gè)討人厭的標(biāo)識(shí)符g嗎?——如果我們能夠當(dāng)g不存在,可以避免多少不必要的麻煩哪。因此,關(guān)鍵就在于始終要通過f或者arguments.callee來引用函數(shù)。如果你使用了命名函數(shù)表達(dá)式,那么應(yīng)該只在調(diào)試的時(shí)候利用那個(gè)名字。最后,還要記住一點(diǎn),一定要把NFE(Named Funciont Expresssions,命名函數(shù)表達(dá)式)聲明期間錯(cuò)誤創(chuàng)建的函數(shù)清理干凈。

嗯,對(duì)于上面最后一點(diǎn),我覺得還要再啰嗦兩句:

JScript的內(nèi)存管理

熟悉上述JScript缺陷之后,再使用這些有毛病的結(jié)構(gòu),就會(huì)發(fā)現(xiàn)內(nèi)存占用方面的潛在問題。下面看一個(gè)簡(jiǎn)單的例子:

  var f = (function(){
    if (true) {
      return function g(){};
    }
    return function g(){};
  })();

我們知道,這里匿名(函數(shù))調(diào)用返回的函數(shù)——帶有標(biāo)識(shí)符g的函數(shù)——被賦值給了外部的f。我們也知道,命名函數(shù)表達(dá)式會(huì)導(dǎo)致產(chǎn)生多余的函數(shù)對(duì)象,而該對(duì)象與返回的函數(shù)對(duì)象不是一回事。由于有一個(gè)多余的g函數(shù)被“截留”在了返回函數(shù)的閉包中,因此內(nèi)存問題就出現(xiàn)了。這是因?yàn)椋╥f語句)內(nèi)部(的)函數(shù)與討厭的g是在同一個(gè)作用域中被聲明的。在這種情況下 ,除非我們顯式地?cái)嚅_對(duì)(匿名調(diào)用返回的)g函數(shù)的引用,否則那個(gè)討厭的家伙會(huì)一直占著內(nèi)存不放。

  var f = (function(){
    var f, g;
    if (true) {
      f = function g(){};
    }
    else {
      f = function g(){};
    }
    // 廢掉g,這樣它就不會(huì)再引用多余的函數(shù)了
    g = null;
    return f;
  })();

請(qǐng)注意,這里也明確聲明了變量g,因此賦值語句g = null就不會(huì)在符合標(biāo)準(zhǔn)的客戶端(如非JScript實(shí)現(xiàn))中創(chuàng)建全局變量g了。通過廢掉對(duì)g的引用,垃圾收集器就可以把g引用的那個(gè)隱式創(chuàng)建的函數(shù)對(duì)象清除了。

在解決JScript NFE內(nèi)存泄漏問題的過程中,我運(yùn)行了一系列簡(jiǎn)單的測(cè)試,以便確定廢掉g能夠釋放內(nèi)存。

測(cè)試

這里的測(cè)試很簡(jiǎn)單。就是通過命名函數(shù)表達(dá)式創(chuàng)建10000個(gè)函數(shù),把它們保存在一個(gè)數(shù)組中。過一會(huì)兒,看看這些函數(shù)到底占用了多少內(nèi)存。然后,再?gòu)U掉這些引用并重復(fù)這一過程。下面是我使用的一個(gè)測(cè)試用例:

  function createFn(){
    return (function(){
      var f;
      if (true) {
        f = function F(){
          return 'standard';
        }
      }
      else if (false) {
        f = function F(){
          return 'alternative';
        }
      }
      else {
        f = function F(){
          return 'fallback';
        }
      }
      // var F = null;
      return f;
    })();
  }

  var arr = [ ];
  for (var i=0; i<10000; i++) {
    arr[i] = createFn();
  }

通過運(yùn)行在Windows XP SP2中的Process Explorer可以看到如下結(jié)果:

  IE6:

    without `null`:   7.6K -> 20.3K
    with `null`:      7.6K -> 18K

  IE7:

    without `null`:   14K -> 29.7K
    with `null`:      14K -> 27K

這個(gè)結(jié)果大致驗(yàn)證了我的想法——顯式地清除多余的引用確實(shí)可以釋放內(nèi)存,但釋放的內(nèi)存空間相對(duì)不多。在創(chuàng)建10000個(gè)函數(shù)對(duì)象的情況下,大約有3MB左右。對(duì)于大型應(yīng)用程序,以及需要長(zhǎng)時(shí)間運(yùn)行或者在低內(nèi)存設(shè)備(如手持設(shè)備)上運(yùn)行的程序而言,這是絕對(duì)需要考慮的。但對(duì)小型腳本而言,這點(diǎn)差別可能也算不了什么。

有讀者可能認(rèn)為本文到此差不多就該結(jié)尾了——實(shí)際上還差得遠(yuǎn)呢 :)。我還想再多談一點(diǎn),這些內(nèi)容涉及的是Safari 2.x。

Safari中存在的bug

在Safari較早的版本——Safari 2.x系列中,也存在一些鮮為人知的與NFE有關(guān)的bug。我在Web上看到有人說Safari 2.x不支持NFE 。實(shí)際上不是那么回事。Safari確實(shí)支持NFE,只不過它的實(shí)現(xiàn)中存在bug而已(很快你就會(huì)看到)。

在某些情況下,Safari 2.x遇到函數(shù)表達(dá)式時(shí)會(huì)出現(xiàn)不能完全解析程序的問題。而且,此時(shí)的Safari不會(huì)拋出任何錯(cuò)誤(例如SyntaxError),只會(huì)“默默地知難而退”:

  (function f(){})(); // <== NFE
  alert(1); // 由于前面的表達(dá)式破壞了整個(gè)程序,因此這一行永遠(yuǎn)不會(huì)執(zhí)行

經(jīng)過多次測(cè)試,我得出一個(gè)結(jié)論:Safari 2.x 不能解析非賦值表達(dá)式中的命名函數(shù)表達(dá)式。下面是一些賦值表達(dá)式的例子:

    // 變量聲明
    var f = 1;

    // 簡(jiǎn)單賦值
    f = 2, g = 3;

    // 返回語句
    (function(){
      return (f = 2);
    })();

換句話說,把命名函數(shù)表達(dá)式放到一個(gè)賦值表達(dá)式中會(huì)讓Safari“很高興”:

  (function f(){}); // 失敗

  var f = function f(){}; // 沒問題

  (function(){
    return function f(){}; // 失敗
  })();

  (function(){
    return (f = function f(){}); // 沒問題
  })();

  setTimeout(function f(){ }, 100); // 失敗
  
  Person.prototype = {
    say: function say() { ... } // 失敗
  }
  
  Person.prototype.say = function say(){ ... }; // 沒問題

同時(shí)這也就意味著,在不使用賦值表達(dá)式的情況下,我們不能使用習(xí)以為常的模式返回命名函數(shù)表達(dá)式:


  // 以下返回命名函數(shù)表達(dá)式的常見模式,對(duì)Safari 2.x來說是不兼容的:
  (function(){
    if (featureTest) {
      return function f(){};
    }
    return function f(){};
  })();

  // 在Safari 2.x中,應(yīng)該使用以下稍麻煩一點(diǎn)的方式:
  (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(){});
  })();

  /* 
    可是,這樣一來,就額外使用了一個(gè)對(duì)函數(shù)的引用,而該引用還被封閉在了返回函數(shù)的閉包中。
	為了最大限度地降低額外的內(nèi)存占用,可以考慮把所有命名函數(shù)表達(dá)式都賦值給一個(gè)變量。
  */

  var __temp;

  (function(){
    if (featureTest) {
      return (__temp = function f(){});
    }
    return (__temp = 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)。雖然這樣做不免會(huì)讓人著急上火,可只要抓住了問題的根源,還是絕對(duì)能夠做到的。

對(duì)了,還有個(gè)小問題必須說明一下:在Safari 2.x中聲明命名函數(shù)時(shí),函數(shù)的字符串表示不會(huì)包含函數(shù)的標(biāo)識(shí)符:

  var f = function g(){};

  // 看到了嗎,函數(shù)的字符串表示中沒有標(biāo)識(shí)符g
  String(f); // function () { }

這不算什么大問題。但正如我以前說過的,函數(shù)的反編譯結(jié)果是無論如何也不能相信的。

SpiderMonkey的怪癖

大家都知道,命名函數(shù)表達(dá)式的標(biāo)識(shí)符只在函數(shù)的局部作用域中有效。但包含這個(gè)標(biāo)識(shí)符的局部作用域又是什么樣子的嗎?其實(shí)非常簡(jiǎn)單。在命名函數(shù)表達(dá)式被求值時(shí),會(huì)創(chuàng)建一個(gè)特殊的對(duì)象,該對(duì)象的唯一目的就是保存一個(gè)屬性,而這個(gè)屬性的名字對(duì)應(yīng)著函數(shù)標(biāo)識(shí)符,屬性的值對(duì)應(yīng)著那個(gè)函數(shù)。這個(gè)對(duì)象會(huì)被注入到當(dāng)前作用域鏈的前端。然后,被“擴(kuò)展”的作用域鏈又被用于初始化函數(shù)。

在這里(想象一下本山大叔在小品《火炬手》中發(fā)表獲獎(jiǎng)感言的情景吧?!g者注),有一點(diǎn)十分有意思,那就是ECMA-262定義這個(gè)(保存函數(shù)標(biāo)識(shí)符的)“特殊”對(duì)象的方式。標(biāo)準(zhǔn)說“像調(diào)用new Object()表達(dá)式那樣”創(chuàng)建這個(gè)對(duì)象。如果從字面上來理解這句話,那么這個(gè)對(duì)象就應(yīng)該是全局Object的一個(gè)實(shí)例。然而,只有一個(gè)實(shí)現(xiàn)是按照標(biāo)準(zhǔn)字面上的要求這么做的,這個(gè)實(shí)現(xiàn)就是SpiderMonkey。因此,在SpiderMonkey中,擴(kuò)展Object.prototype有可能會(huì)干擾函數(shù)的局部作用域:

  
  Object.prototype.x = 'outer';
  
  (function(){
    
    var x = 'inner';
    
    /*
      函數(shù)foo的作用域鏈中有一個(gè)特殊的對(duì)象——用于保存函數(shù)的標(biāo)識(shí)符。這個(gè)特殊的對(duì)象實(shí)際上就是{ foo: <function object> }。
      當(dāng)通過作用域鏈解析x時(shí),首先解析的是foo的局部環(huán)境。如果沒有找到x,則繼續(xù)搜索作用域鏈中的下一個(gè)對(duì)象。下一個(gè)對(duì)象
      就是保存函數(shù)標(biāo)識(shí)符的那個(gè)對(duì)象——{ foo: <function object> },由于該對(duì)象繼承自O(shè)bject.prototype,所以在此可以找到x。
      而這個(gè)x的值也就是Object.prototype.x的值(outer)。結(jié)果,外部函數(shù)的作用域(包含x = 'inner'的作用域)就不會(huì)被解析了。
    */
    
    (function foo(){
      
      alert(x); // 提示框中顯示:outer
    
    })();
  })();
  

不過,更高版本的SpiderMonkey改變了上述行為,原因可能是認(rèn)為那是一個(gè)安全漏洞。也就是說,“特殊”對(duì)象不再繼承Object.prototype了。不過,如果你使用Firefox 3或者更低版本,還可以“重溫”這種行為。

另一個(gè)把內(nèi)部對(duì)象實(shí)現(xiàn)為全局Object對(duì)象的是黑莓(Blackberry)瀏覽器。目前,它的活動(dòng)對(duì)象(Activation Object)仍然繼承Object.prototype??墒?,ECMA-262并沒有說活動(dòng)對(duì)象也要“像調(diào)用new Object()表達(dá)式那樣”來創(chuàng)建(或者說像創(chuàng)建保存NFE標(biāo)識(shí)符的對(duì)象一樣創(chuàng)建)。 人家規(guī)范只說了活動(dòng)對(duì)象是規(guī)范中的一種機(jī)制。

好,那我們下面就來看看黑莓瀏覽器的行為吧:

  
  Object.prototype.x = 'outer';
  
  (function(){
    
    var x = 'inner';
    
    (function(){
      
      /*
      在沿著作用域鏈解析x的過程中,首先會(huì)搜索局部函數(shù)的活動(dòng)對(duì)象。當(dāng)然,在該對(duì)象中找不到x。
      可是,由于活動(dòng)對(duì)象繼承自O(shè)bject.prototype,因此搜索x的下一個(gè)目標(biāo)就是Object.prototype;而
      Object.prototype中又確實(shí)有x的定義。結(jié)果,x的值就被解析為——outer。跟前面的例子差不多,
      包含x = 'inner'的外部函數(shù)的作用域(活動(dòng)對(duì)象)就不會(huì)被解析了。
      */
      
      alert(x); // 提示框中顯示:outer
      
    })();
  })();

雖然這有點(diǎn)讓人不可思議,但更令人匪夷所思的則是函數(shù)中的變量甚至?xí)c已有的Object.prototype的成員發(fā)生沖突:

  (function(){
    
    var constructor = function(){ return 1; };
    
    (function(){
      
      constructor(); // 求值結(jié)果是{}(即相當(dāng)于調(diào)用了Object.prototype.constructor()?!g者注)而不是1
      
      constructor === Object.prototype.constructor; // true
      toString === Object.prototype.toString; // true
      
      // ……
      
    })();
  })();

解決方案

  var fn = (function(){

    // 聲明要引用函數(shù)的變量
    var f;

    // 有條件地創(chuàng)建命名函數(shù)
    // 并將其引用賦值給f
    if (true) {
      f = function F(){ }
    }
    else if (false) {
      f = function F(){ }
    }
    else {
      f = function F(){ }
    }

    // 聲明一個(gè)與函數(shù)名(標(biāo)識(shí)符)對(duì)應(yīng)的變量,并賦值為null
    // 這實(shí)際上是給相應(yīng)標(biāo)識(shí)符引用的函數(shù)對(duì)象作了一個(gè)標(biāo)記,
    // 以便垃圾回收器知道可以回收它了
    var F = null;

    // 返回根據(jù)條件定義的函數(shù)
    return f;
  })();

最后,我要給出一個(gè)應(yīng)用上述“技術(shù)”的實(shí)例。這是一個(gè)跨瀏覽器的addEvent函數(shù)的代碼:


  // 1) 使用獨(dú)立的作用域包含聲明
  var addEvent = (function(){

    var docEl = document.documentElement;

    // 2) 聲明要引用函數(shù)的變量
    var fn;

    if (docEl.addEventListener) {

      // 3) 有意給函數(shù)一個(gè)描述性的標(biāo)識(shí)符
      fn = function addEvent(element, eventName, callback) {
        element.addEventListener(eventName, callback, false);
      }
    }
    else if (docEl.attachEvent) {
      fn = function addEvent(element, eventName, callback) {
        element.attachEvent('on' + eventName, callback);
      }
    }
    else {
      fn = function addEvent(element, eventName, callback) {
        element['on' + eventName] = callback;
      }
    }

    // 4) 清除由JScript創(chuàng)建的addEvent函數(shù)
    //    一定要保證在賦值前使用var關(guān)鍵字
    //    除非函數(shù)頂部已經(jīng)聲明了addEvent
    var addEvent = null;

    // 5) 最后返回由fn引用的函數(shù)
    return fn;
  })();

替代方案

不要忘了,如果我們不想在調(diào)用棧中保留描述性的名字,實(shí)際上還有其他選擇。換句話說,就是還存在不必使用命名函數(shù)表達(dá)式的方案。首先,很多時(shí)候都可以通過聲明而非表達(dá)式定義函數(shù)。這個(gè)方案只適合不需要?jiǎng)?chuàng)建多個(gè)函數(shù)的情形:

  var hasClassName = (function(){

    // 定義私有變量
    var cache = { };

    // 使用函數(shù)聲明
    function hasClassName(element, className) {
      var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)';
      var re = cache[_className] || (cache[_className] = new RegExp(_className));
      return re.test(element.className);
    }

    // 返回函數(shù)
    return hasClassName;
  })();

顯然,當(dāng)存在多個(gè)分支函數(shù)定義時(shí),這個(gè)方案就不能勝任了。不過,我最早見過托比·蘭吉(Tobiel Langel)使用過一個(gè)很有味道的模式。他的這種模式是提前使用函數(shù)聲明來定義所有函數(shù),并分別為這些函數(shù)指定不同的標(biāo)識(shí)符

  var addEvent = (function(){

    var docEl = document.documentElement;

    function addEventListener(){
      /* ... */
    }
    function attachEvent(){
      /* ... */
    }
    function addEventAsProperty(){
      /* ... */
    }

    if (typeof docEl.addEventListener != 'undefined') {
      return addEventListener;
    }
    elseif (typeof docEl.attachEvent != 'undefined') {
      return attachEvent;
    }
    return addEventAsProperty;
  })();

雖然這個(gè)方案很優(yōu)雅,但也不是沒有缺點(diǎn)。第一,由于使用不同的標(biāo)識(shí)符,導(dǎo)致喪失了命名的一致性。且不說這樣好還是壞,最起碼它不夠清晰。有人喜歡使用相同的名字,但也有人根本不在乎字眼上的差別。可畢竟,不同的名字會(huì)讓人聯(lián)想到所用的不同實(shí)現(xiàn)。例如,在調(diào)試器中看到attachEvent,我們就知道addEvent是基于attachEvent的實(shí)現(xiàn)(即基于IE的事件模型?!g者注)。當(dāng)然,基于實(shí)現(xiàn)來命名的方式也不一定都行得通。假如我們要提供一個(gè)API,并按照這種方式把函數(shù)命名為inner。那么API用戶的很容易就會(huì)被相應(yīng)實(shí)現(xiàn)的細(xì)節(jié)搞得暈頭轉(zhuǎn)向。(也許是因?yàn)閕nner這個(gè)名字太通用,不同實(shí)現(xiàn)中可能都會(huì)有,因此容易讓人分不清這個(gè)API到底基于哪個(gè)實(shí)現(xiàn)。——譯者注)

要解決這個(gè)問題,當(dāng)然就得想一套更合理的命名方案了。但關(guān)鍵是不要再額外制造麻煩。我現(xiàn)在能想起來的方案大概有如下幾個(gè):

  'addEvent', 'altAddEvent', 'fallbackAddEvent'
  // 或者
  'addEvent', 'addEvent2', 'addEvent3'
  // 或者
  'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'

另外,托比使用的模式還存在一個(gè)小問題,即增加內(nèi)存占用。提前創(chuàng)建N個(gè)不同名字的函數(shù),等于有N-1的函數(shù)是用不到的。具體來講,如果document.documentElement中包含attachEvent,那么addEventListeneraddEventAsProperty則根本就用不著了??墒?,他們都占著內(nèi)存哪;而且,這些內(nèi)存將永遠(yuǎn)都得不到釋放,原因跟JScript臭哄哄的命名表達(dá)式相同——這兩個(gè)函數(shù)都被“截留”在返回的那個(gè)函數(shù)的閉包中了。

不過,增加內(nèi)存占用這個(gè)問題確實(shí)沒什么大不了的。如果某個(gè)庫(kù)——例如Prototype.js——采用了這種模式,無非也就是多創(chuàng)建一兩百個(gè)函數(shù)而已。只要不是(在運(yùn)行時(shí))重復(fù)地創(chuàng)建這些函數(shù),而是只(在加載時(shí))創(chuàng)建一次,那么就沒有什么好擔(dān)心的。

WebKit的displayName

WebKit團(tuán)隊(duì)在這個(gè)問題采取了有點(diǎn)兒另類的策略。囿于函數(shù)(包括匿名和命名函數(shù))如此之差的表現(xiàn)力,WebKit引入了一個(gè)“特殊的”displayName屬性(本質(zhì)上是一個(gè)字符串),如果開發(fā)人員為函數(shù)的這個(gè)屬性賦值,則該屬性的值將在調(diào)試器或性能分析器中被顯示在函數(shù)“名稱”的位置上。弗朗西斯科·托依瑪斯基(Francisco Tolmasky)詳細(xì)地解釋了這個(gè)策略的原理和實(shí)現(xiàn)

對(duì)未來的思考

將來的ECMAScript-262第5版(目前還是草案)會(huì)引入所謂的嚴(yán)格模式(strict mode)。開啟嚴(yán)格模式的實(shí)現(xiàn)會(huì)禁用語言中的那些不穩(wěn)定、不可靠和不安全的特性。據(jù)說出于安全方面的考慮,arguments.callee屬性將在嚴(yán)格模式下被“封殺”。因此,在處于嚴(yán)格模式時(shí),訪問arguments.callee會(huì)導(dǎo)致TypeError(參見ECMA-262第5版的10.6節(jié))。而我之所以在此提到嚴(yán)格模式,是因?yàn)槿绻诨诘?版標(biāo)準(zhǔn)的實(shí)現(xiàn)中無法使用arguments.callee來執(zhí)行遞歸操作,那么使用命名函數(shù)表達(dá)式的可能性就會(huì)大大增加。從這個(gè)意義上來說,理解命名函數(shù)表達(dá)式的語義及其bug也就顯得更加重要了。

  // 此前,你可能會(huì)使用arguments.callee
  (function(x) {
    if (x <= 1) return 1;
    return x * arguments.callee(x - 1);
  })(10);
  
  // 但在嚴(yán)格模式下,有可能就要使用命名函數(shù)表達(dá)式
  (function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
  })(10);
  
  // 要么就退一步,使用沒有那么靈活的函數(shù)聲明
  function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
  }
  factorial(10);

致謝

理查德· 康福德(Richard Cornford),是他率先解釋了JScript中命名函數(shù)表達(dá)式所存在的bug。理查德解釋了我在這篇文章中提及的大多數(shù)bug,所以我強(qiáng)烈建議大家去看看他的解釋。我還要感謝Yann-Erwan Perio(這是中國(guó)人嗎?——譯者注)道格拉斯·克勞克佛德(Douglas Crockford),他們?cè)缭?003年就在comp.lang.javascript論壇中提及并討論NFE問題了

約翰-戴維·道爾頓(John-David Dalton)對(duì)“最終解決方案”提出了很好的建議。

托比·蘭吉的點(diǎn)子被我用在了“替代方案”中。

蓋瑞特·史密斯(Garrett Smith)德米特里·蘇斯尼科(Dmitry Soshnikov)對(duì)本文的多方面作出了補(bǔ)充和修正。

要提建議或者反饋錯(cuò)誤嗎?可以mailto:kangax@gmail.com給我寫封郵件,隨便,怎么都行。

發(fā)表時(shí)間:2009年6月17日 最近修改:2009年10月9日