在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ HTML/ 揭秘命名函數(shù)表達(dá)式
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計(jì)模式之狀態(tài)模式
JavaScript 核心(晉級(jí)高手必讀篇)
設(shè)計(jì)模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計(jì)模式之中介者模式
設(shè)計(jì)模式之裝飾者模式
設(shè)計(jì)模式之模板方法
設(shè)計(jì)模式之外觀模式
強(qiáng)大的原型和原型鏈
設(shè)計(jì)模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計(jì)模式之適配器模式
設(shè)計(jì)模式之組合模式
設(shè)計(jì)模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫(xiě)高質(zhì)量 JavaScript 代碼的基本要點(diǎn)
求值策略
閉包(Closures)
對(duì)象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計(jì)模式之代理模式
變量對(duì)象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計(jì)模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴(lài)倒置原則 DIP
設(shè)計(jì)模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計(jì)模式之享元模式
設(shè)計(jì)模式之原型模式
根本沒(méi)有“JSON 對(duì)象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
全面解析 Module 模式
對(duì)象創(chuàng)建模式(下篇)
設(shè)計(jì)模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開(kāi)閉原則 OCP
設(shè)計(jì)模式之橋接模式
設(shè)計(jì)模式之策略模式
設(shè)計(jì)模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計(jì)模式之工廠模式

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

前言

網(wǎng)上還沒(méi)用發(fā)現(xiàn)有人對(duì)命名函數(shù)表達(dá)式進(jìn)去重復(fù)深入的討論,正因?yàn)槿绱?,網(wǎng)上出現(xiàn)了各種各樣的誤解,本文將從原理和實(shí)踐兩個(gè)方面來(lái)探討 JavaScript 關(guān)于命名函數(shù)表達(dá)式的優(yōu)缺點(diǎn)。

簡(jiǎn)單的說(shuō),命名函數(shù)表達(dá)式只有一個(gè)用戶(hù),那就是在 Debug 或者 Profiler 分析的時(shí)候來(lái)描述函數(shù)的名稱(chēng),也可以使用函數(shù)名實(shí)現(xiàn)遞歸,但很快你就會(huì)發(fā)現(xiàn)其實(shí)是不切實(shí)際的。當(dāng)然,如果你不關(guān)注調(diào)試,那就沒(méi)什么可擔(dān)心的了,否則,如果你想了解兼容性方面的東西的話(huà),你還是應(yīng)該繼續(xù)往下看看。

我們先開(kāi)始看看,什么叫函數(shù)表達(dá)式,然后再說(shuō)一下現(xiàn)代調(diào)試器如何處理這些表達(dá)式,如果你已經(jīng)對(duì)這方面很熟悉的話(huà),請(qǐng)直接跳過(guò)此小節(jié)。

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

在 ECMAScript 中,創(chuàng)建函數(shù)的最常用的兩個(gè)方法是函數(shù)表達(dá)式和函數(shù)聲明,兩者期間的區(qū)別是有點(diǎn)暈,因?yàn)?ECMA 規(guī)范只明確了一點(diǎn):函數(shù)聲明必須帶有標(biāo)示符(Identifier)(就是大家常說(shuō)的函數(shù)名稱(chēng)),而函數(shù)表達(dá)式則可以省略這個(gè)標(biāo)示符:

函數(shù)聲明:

function 函數(shù)名稱(chēng) (參數(shù):可選){ 函數(shù)體 }

函數(shù)表達(dá)式:

function 函數(shù)名稱(chēng)(可選)(參數(shù):可選){ 函數(shù)體 }

所以,可以看出,如果不聲明函數(shù)名稱(chēng),它肯定是表達(dá)式,可如果聲明了函數(shù)名稱(chēng)的話(huà),如何判斷是函數(shù)聲明還是函數(shù)表達(dá)式呢?ECMAScript 是通過(guò)上下文來(lái)區(qū)分的,如果 function foo(){}是作為賦值表達(dá)式的一部分的話(huà),那它就是一個(gè)函數(shù)表達(dá)式,如果 function foo(){}被包含在一個(gè)函數(shù)體內(nèi),或者位于程序的最頂部的話(huà),那它就是一個(gè)函數(shù)聲明。

  function foo(){} // 聲明,因?yàn)樗浅绦虻囊徊糠?  var bar = function foo(){}; // 表達(dá)式,因?yàn)樗琴x值表達(dá)式的一部分
  new function bar(){}; // 表達(dá)式,因?yàn)樗莕ew表達(dá)式
  (function(){
    function bar(){} // 聲明,因?yàn)樗呛瘮?shù)體的一部分
  })();

還有一種函數(shù)表達(dá)式不太常見(jiàn),就是被括號(hào)括住的(function foo(){}),他是表達(dá)式的原因是因?yàn)槔ㄌ?hào) ()是一個(gè)分組操作符,它的內(nèi)部只能包含表達(dá)式,我們來(lái)看幾個(gè)例子:

  function foo(){} // 函數(shù)聲明
  (function foo(){}); // 函數(shù)表達(dá)式:包含在分組操作符內(nèi)
  try {
    (var x = 5); // 分組操作符,只能包含表達(dá)式而不能包含語(yǔ)句:這里的var就是語(yǔ)句
  } catch(err) {
    // SyntaxError
  }

你可以會(huì)想到,在使用 eval 對(duì) JSON 進(jìn)行執(zhí)行的時(shí)候,JSON 字符串通常被包含在一個(gè)圓括號(hào)里:eval('(' + json + ')'),這樣做的原因就是因?yàn)榉纸M操作符,也就是這對(duì)括號(hào),會(huì)讓解析器強(qiáng)制將JSON的花括號(hào)解析成表達(dá)式而不是代碼塊。

  try {
    { "x": 5 }; // "{" 和 "}" 做解析成代碼塊
  } catch(err) {
    // SyntaxError
  }
  ({ "x": 5 }); // 分組操作符強(qiáng)制將"{" 和 "}"作為對(duì)象字面量來(lái)解析

表達(dá)式和聲明存在著十分微妙的差別,首先,函數(shù)聲明會(huì)在任何表達(dá)式被解析和求值之前先被解析和求值,即使你的聲明在代碼的最后一行,它也會(huì)在同作用域內(nèi)第一個(gè)表達(dá)式之前被解析/求值,參考如下例子,函數(shù) fn 是在 alert 之后聲明的,但是在 alert 執(zhí)行的時(shí)候,fn已經(jīng)有定義了:

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

另外,還有一點(diǎn)需要提醒一下,函數(shù)聲明在條件語(yǔ)句內(nèi)雖然可以用,但是沒(méi)有被標(biāo)準(zhǔn)化,也就是說(shuō)不同的環(huán)境可能有不同的執(zhí)行結(jié)果,所以這樣情況下,最好使用函數(shù)表達(dá)式:

  // 千萬(wàn)別這樣做!
  // 因?yàn)橛械臑g覽器會(huì)返回first的這個(gè)function,而有的瀏覽器返回的卻是第二個(gè)
  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ī)則如下:

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

函數(shù)語(yǔ)句

在 ECMAScript 的語(yǔ)法擴(kuò)展中,有一個(gè)是函數(shù)語(yǔ)句,目前只有基于 Gecko 的瀏覽器實(shí)現(xiàn)了該擴(kuò)展,所以對(duì)于下面的例子,我們僅是抱著學(xué)習(xí)的目的來(lái)看,一般來(lái)說(shuō)不推薦使用(除非你針對(duì) Gecko 瀏覽器進(jìn)行開(kāi)發(fā))。

1.一般語(yǔ)句能用的地方,函數(shù)語(yǔ)句也能用,當(dāng)然也包括 Block 塊中:

  if (true) {
    function f(){ }
  }
  else {
    function f(){ }
  }

2.函數(shù)語(yǔ)句可以像其他語(yǔ)句一樣被解析,包含基于條件執(zhí)行的情形

  if (true) {
    function foo(){ return 1; }
  }
  else {
    function foo(){ return 2; }
  }
  foo(); // 1
  // 注:其它客戶(hù)端會(huì)將foo解析成函數(shù)聲明 
  // 因此,第二個(gè)foo會(huì)覆蓋第一個(gè),結(jié)果返回2,而不是1

3.函數(shù)語(yǔ)句不是在變量初始化期間聲明的,而是在運(yùn)行時(shí)聲明的——與函數(shù)表達(dá)式一樣。不過(guò),函數(shù)語(yǔ)句的標(biāo)識(shí)符一旦聲明能在函數(shù)的整個(gè)作用域生效了。標(biāo)識(shí)符有效性正是導(dǎo)致函數(shù)語(yǔ)句與函數(shù)表達(dá)式不同的關(guān)鍵所在(下一小節(jié)我們將會(huì)展示命名函數(shù)表達(dá)式的具體行為)。

  // 此刻,foo還沒(méi)用聲明
  typeof foo; // "undefined"
  if (true) {
    // 進(jìn)入這里以后,foo就被聲明在整個(gè)作用域內(nèi)了
    function foo(){ return 1; }
  }
  else {
    // 從來(lái)不會(huì)走到這里,所以這里的foo也不會(huì)被聲明
    function foo(){ return 2; }
  }
  typeof foo; // "function"

不過(guò),我們可以使用下面這樣的符合標(biāo)準(zhǔn)的代碼來(lái)模式上面例子中的函數(shù)語(yǔ)句:

  var foo;
  if (true) {
    foo = function foo(){ return 1; };
  }
  else {
    foo = function foo() { return 2; };
  }

4.函數(shù)語(yǔ)句和函數(shù)聲明(或命名函數(shù)表達(dá)式)的字符串表示類(lèi)似,也包括標(biāo)識(shí)符:

  if (true) {
    function foo(){ return 1; }
  }
  String(foo); // function foo() { return 1; }

5.另外一個(gè),早期基于 Gecko 的實(shí)現(xiàn)(Firefox 3 及以前版本)中存在一個(gè) bug,即函數(shù)語(yǔ)句覆蓋函數(shù)聲明的方式不正確。在這些早期的實(shí)現(xiàn)中,函數(shù)語(yǔ)句不知何故不能覆蓋函數(shù)聲明:

  // 函數(shù)聲明
  function foo(){ return 1; }
  if (true) {
    // 用函數(shù)語(yǔ)句重寫(xiě)
    function foo(){ return 2; }
  }
  foo(); // FF3以下返回1,F(xiàn)F3.5以上返回2
  // 不過(guò),如果前面是函數(shù)表達(dá)式,則沒(méi)用問(wèn)題
  var foo = function(){ return 1; };
  if (true) {
    function foo(){ return 2; }
  }
  foo(); // 所有版本都返回2

再次強(qiáng)調(diào)一點(diǎn),上面這些例子只是在某些瀏覽器支持,所以推薦大家不要使用這些,除非你就在特性的瀏覽器上做開(kāi)發(fā)。

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

函數(shù)表達(dá)式在實(shí)際應(yīng)用中還是很常見(jiàn)的,在 web 開(kāi)發(fā)中友個(gè)常用的模式是基于對(duì)某種特性的測(cè)試來(lái)偽裝函數(shù)定義,從而達(dá)到性能優(yōu)化的目的,但由于這種方式都是在同一作用域內(nèi),所以基本上一定要用函數(shù)表達(dá)式:

  // 該代碼來(lái)自Garrett Smith的APE Javascript library庫(kù)(http://dhtmlkitchen.com/ape/) 
  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á)式,理所當(dāng)然,就是它得有名字,前面的例子 var bar = function foo(){};就是一個(gè)有效的命名函數(shù)表達(dá)式,但有一點(diǎn)需要記?。哼@個(gè)名字只在新定義的函數(shù)作用域內(nèi)有效,因?yàn)橐?guī)范規(guī)定了標(biāo)示符不能在外圍的作用域內(nèi)有效:

  var f = function foo(){
    return typeof foo; // foo是在內(nèi)部作用域內(nèi)有效
  };
  // foo在外部用于是不可見(jiàn)的
  typeof foo; // "undefined"
  f(); // "function"

既然,這么要求,那命名函數(shù)表達(dá)式到底有啥用???為啥要取名?

正如我們開(kāi)頭所說(shuō):給它一個(gè)名字就是可以讓調(diào)試過(guò)程更方便,因?yàn)樵谡{(diào)試的時(shí)候,如果在調(diào)用棧中的每個(gè)項(xiàng)都有自己的名字來(lái)描述,那么調(diào)試過(guò)程就太爽了,感受不一樣嘛。

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

如果一個(gè)函數(shù)有名字,那調(diào)試器在調(diào)試的時(shí)候會(huì)將它的名字顯示在調(diào)用的棧上。有些調(diào)試器(Firebug)有時(shí)候還會(huì)為你們函數(shù)取名并顯示,讓他們和那些應(yīng)用該函數(shù)的便利具有相同的角色,可是通常情況下,這些調(diào)試器只安裝簡(jiǎn)單的規(guī)則來(lái)取名,所以說(shuō)沒(méi)有太大價(jià)格,我們來(lái)看一個(gè)例子:

  function foo(){
    return bar();
  }
  function bar(){
    return baz();
  }
  function baz(){
    debugger;
  }
  foo();
  // 這里我們使用了3個(gè)帶名字的函數(shù)聲明
  // 所以當(dāng)調(diào)試器走到debugger語(yǔ)句的時(shí)候,F(xiàn)irebug的調(diào)用棧上看起來(lái)非常清晰明了 
  // 因?yàn)楹苊靼椎仫@示了名稱(chēng)
  baz
  bar
  foo
  expr_test.html()

通過(guò)查看調(diào)用棧的信息,我們可以很明了地知道 foo 調(diào)用了 bar,bar 又調(diào)用了 baz(而 foo 本身有在 expr_test.html 文檔的全局作用域內(nèi)被調(diào)用),不過(guò),還有一個(gè)比較爽地方,就是剛才說(shuō)的 Firebug 為匿名表達(dá)式取名的功能:

  function foo(){
    return bar();
  }
  var bar = function(){
    return baz();
  }
  function baz(){
    debugger;
  }
  foo();
  // Call stack
  baz
  bar() //看到了么? 
  foo
  expr_test.html()

然后,當(dāng)函數(shù)表達(dá)式稍微復(fù)雜一些的時(shí)候,調(diào)試器就不那么聰明了,我們只能在調(diào)用棧中看到問(wèn)號(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();
  // Call stack
  baz
  (?)() // 這里可是問(wèn)號(hào)哦
  foo
  expr_test.html()

另外,當(dāng)把函數(shù)賦值給多個(gè)變量的時(shí)候,也會(huì)出現(xiàn)令人郁悶的問(wèn)題:

  function foo(){
    return baz();
  }
  var bar = function(){
    debugger;
  };
  var baz = bar;
  bar = function() { 
    alert('spoofed');
  };
  foo();
  // Call stack:
  bar()
  foo
  expr_test.html()

這時(shí)候,調(diào)用棧顯示的是 foo 調(diào)用了 bar,但實(shí)際上并非如此,之所以有這種問(wèn)題,是因?yàn)?baz 和另外一個(gè)包含 alert('spoofed')的函數(shù)做了引用交換所導(dǎo)致的。

歸根結(jié)底,只有給函數(shù)表達(dá)式取個(gè)名字,才是最委托的辦法,也就是使用命名函數(shù)表達(dá)式。我們來(lái)使用帶名字的表達(dá)式來(lái)重寫(xiě)上面的例子(注意立即調(diào)用的表達(dá)式塊里返回的 2 個(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()

OK,又學(xué)了一招吧?不過(guò)在高興之前,我們?cè)倏纯床煌瑢こ5?JScript 吧。

JScript的Bug

比較惡的是,IE 的 ECMAScrip t實(shí)現(xiàn) JScript 嚴(yán)重混淆了命名函數(shù)表達(dá)式,搞得現(xiàn)很多人都出來(lái)反對(duì)命名函數(shù)表達(dá)式,而且即便是最新的一版(IE8 中使用的 5.8 版)仍然存在下列問(wèn)題。

下面我們就來(lái)看看IE在實(shí)現(xiàn)中究竟犯了那些錯(cuò)誤,俗話(huà)說(shuō)知已知彼,才能百戰(zhàn)不殆。我們來(lái)看看如下幾個(gè)例子:

例 1:函數(shù)表達(dá)式的標(biāo)示符泄露到外部作用域

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

上面我們說(shuō)過(guò),命名函數(shù)表達(dá)式的標(biāo)示符在外部作用域是無(wú)效的,但 JScript 明顯是違反了這一規(guī)范,上面例子中的標(biāo)示符 g 被解析成函數(shù)對(duì)象,這就亂了套了,很多難以發(fā)現(xiàn)的 bug 都是因?yàn)檫@個(gè)原因?qū)е碌摹?/p>

注:IE9 貌似已經(jīng)修復(fù)了這個(gè)問(wèn)題

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

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

特性環(huán)境下,函數(shù)聲明會(huì)優(yōu)先于任何表達(dá)式被解析,上面的例子展示的是 JScript 實(shí)際上是把命名函數(shù)表達(dá)式當(dāng)成函數(shù)聲明了,因?yàn)樗趯?shí)際聲明之前就解析了 g。

這個(gè)例子引出了下一個(gè)例子。

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

    var f = function g(){};
    f === g; // false
    f.expando = 'foo';
    g.expando; // undefined

看到這里,大家會(huì)覺(jué)得問(wèn)題嚴(yán)重了,因?yàn)樾薷娜魏我粋€(gè)對(duì)象,另外一個(gè)沒(méi)有什么改變,這太惡了。通過(guò)這個(gè)例子可以發(fā)現(xiàn),創(chuàng)建 2 個(gè)不同的對(duì)象,也就是說(shuō)如果你想修改f的屬性中保存某個(gè)信息,然后想當(dāng)然地通過(guò)引用相同對(duì)象的 g 的同名屬性來(lái)使用,那問(wèn)題就大了,因?yàn)楦揪筒豢赡堋?/p>

再來(lái)看一個(gè)稍微復(fù)雜的例子:

例 4:僅僅順序解析函數(shù)聲明而忽略條件語(yǔ)句塊

    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ù)聲明不受條件代碼塊約束,所以在這個(gè)很惡的 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)入這個(gè)可惡 if 分支,因此 f 就會(huì)繼續(xù)引用第一個(gè)函數(shù) function g(){ return 1 }。分析到這里,問(wèn)題就很清楚了:假如你不夠細(xì)心,在f中調(diào)用了 g,那么將會(huì)調(diào)用一個(gè)毫不相干的 g 函數(shù)對(duì)象。

你可能會(huì)問(wèn),將不同的對(duì)象和 arguments.callee 相比較時(shí),有什么樣的區(qū)別呢?我們來(lái)看看:

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

可以看到,arguments.callee 的引用一直是被調(diào)用的函數(shù),實(shí)際上這也是好事,稍后會(huì)解釋。

還有一個(gè)有趣的例子,那就是在不包含聲明的賦值語(yǔ)句中使用命名函數(shù)表達(dá)式:

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

按照代碼的分析,我們?cè)臼窍雱?chuàng)建一個(gè)全局屬性 f(注意不要和一般的匿名函數(shù)混淆了,里面用的是帶名字的生命),JScript 在這里搗亂了一把,首先他把表達(dá)式當(dāng)成函數(shù)聲明解析了,所以左邊的f被聲明為局部變量了(和一般的匿名函數(shù)里的聲明一樣),然后在函數(shù)執(zhí)行的時(shí)候,f已經(jīng)是定義過(guò)的了,右邊的 function f(){}則直接就賦值給局部變量f了,所以f根本就不是全局屬性。

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

對(duì)于,上面最后一點(diǎn),我們還得再解釋一下。

JScript 的內(nèi)存管理

知道了這些不符合規(guī)范的代碼解析 bug 以后,我們?nèi)绻盟脑?huà),就會(huì)發(fā)現(xiàn)內(nèi)存方面其實(shí)是有問(wèn)題的,來(lái)看一個(gè)例子:

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

我們知道,這個(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)存問(wèn)題就出現(xiàn)了。這是因?yàn)?if 語(yǔ)句內(nèi)部的函數(shù)與 g 是在同一個(gè)作用域中被聲明的。這種情況下 ,除非我們顯式斷開(kāi)對(duì) g 函數(shù)的引用,否則它一直占著內(nèi)存不放。

  var f = (function(){
    var f, g;
    if (true) {
      f = function g(){};
    }
    else {
      f = function g(){};
    }
    // 設(shè)置g為null以后它就不會(huì)再占內(nèi)存了
    g = null;
    return f;
  })();

通過(guò)設(shè)置g為null,垃圾回收器就把g引用的那個(gè)隱式函數(shù)給回收掉了,為了驗(yàn)證我們的代碼,我們來(lái)做一些測(cè)試,以確保我們的內(nèi)存被回收了。

測(cè)試

測(cè)試很簡(jiǎn)單,就是命名函數(shù)表達(dá)式創(chuàng)建 10000 個(gè)函數(shù),然后把它們保存在一個(gè)數(shù)組中。等一會(huì)兒以后再看這些函數(shù)到底占用了多少內(nèi)存。然后,再斷開(kāi)這些引用并重復(fù)這一過(guò)程。下面是測(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();
  }

通過(guò)運(yùn)行在 Windows XP SP2 中的任務(wù)管理器可以看到如下結(jié)果:

IE6:

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

IE7:

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

如我們所料,顯示斷開(kāi)引用可以釋放內(nèi)存,但是釋放的內(nèi)存不是很多,10000 個(gè)函數(shù)對(duì)象才釋放大約 3M 的內(nèi)存,這對(duì)一些小型腳本不算什么,但對(duì)于大型程序,或者長(zhǎng)時(shí)間運(yùn)行在低內(nèi)存的設(shè)備里的時(shí)候,這是非常有必要的。

關(guān)于在 Safari 2.x 中 JS 的解析也有一些 bug,但介于版本比較低,所以我們?cè)谶@里就不介紹了,大家如果想看的話(huà),請(qǐng)仔細(xì)查看英文資料。

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ù)。

在這里,有一點(diǎn)十分有意思,那就是 ECMA-262 定義這個(gè)(保存函數(shù)標(biāo)識(shí)符的)“特殊”對(duì)象的方式。標(biāo)準(zhǔn)說(shuō)“像調(diào)用 new Object()表達(dá)式那樣”創(chuàng)建這個(gè)對(duì)象。如果從字面上來(lái)理解這句話(huà),那么這個(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)通過(guò)作用域鏈解析x時(shí),首先解析的是foo的局部環(huán)境。如果沒(méi)有找到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
    })();
  })();

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

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

那我們就來(lái)看看黑莓里都發(fā)生了什么:

  Object.prototype.x = 'outer';
  (function(){
    var x = 'inner';
    (function(){
      /*
      在沿著作用域鏈解析x的過(guò)程中,首先會(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
    })();
  })();

不過(guò)神奇的還是,函數(shù)中的變量甚至?xí)c已有的 Object.prototype 的成員發(fā)生沖突,來(lái)看看下面的代碼:

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

要避免這個(gè)問(wèn)題,要避免使用 Object.prototype 里的屬性名稱(chēng),如 toString,valueOf, hasOwnProperty 等等。

JScript解決方案

  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ù)的應(yīng)用實(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;
  })();

替代方案

其實(shí),如果我們不想要這個(gè)描述性名字的話(huà),我們就可以用最簡(jiǎn)單的形式來(lái)做,也就是在函數(shù)內(nèi)部聲明一個(gè)函數(shù)(而不是函數(shù)表達(dá)式),然后返回該函數(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è)方案就不行了。不過(guò)有種模式貌似可以實(shí)現(xiàn):那就是提前使用函數(shù)聲明來(lái)定義所有函數(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)雅,但也不是沒(méi)有缺點(diǎn)。第一,由于使用不同的標(biāo)識(shí)符,導(dǎo)致喪失了命名的一致性。且不說(shuō)這樣好還是壞,最起碼它不夠清晰。有人喜歡使用相同的名字,但也有人根本不在乎字眼上的差別??僧吘?,不同的名字會(huì)讓人聯(lián)想到所用的不同實(shí)現(xiàn)。例如,在調(diào)試器中看到 attachEvent,我們就知 道 addEvent 是基于 attachEvent 的實(shí)現(xiàn)。當(dāng)然,基于實(shí)現(xiàn)來(lái)命名的方式也不一定都行得通。假如我們要提供一個(gè) API,并按照這種方式把函數(shù)命名為 inner。那么 API 用戶(hù)的很容易就會(huì)被相應(yīng)實(shí)現(xiàn)的 細(xì)節(jié)搞得暈頭轉(zhuǎn)向。

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

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

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

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

WebKit 的 displayName

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

未來(lái)考慮

將來(lái)的 ECMAScript-262 第 5 版(目前還是草案)會(huì)引入所謂的嚴(yán)格模式(strict mode)。開(kāi)啟嚴(yán)格模式的實(shí)現(xiàn)會(huì)禁用語(yǔ)言中的那些不穩(wěn)定、不可靠和不安全的特性。據(jù)說(shuō)出于安全方面的考慮,arguments.callee 屬性將在嚴(yán)格模式下被“封殺”。因此,在處于嚴(yán)格模式時(shí),訪(fǎng)問(wèn) arguments.callee 會(huì)導(dǎo)致 TypeError(參見(jiàn) ECMA-262 第 5 版的 10.6 節(jié))。而我之所以在此提到嚴(yán)格模式,是因?yàn)槿绻诨诘?5 版標(biāo)準(zhǔn)的實(shí)現(xiàn)中無(wú)法使用 arguments.callee 來(lái)執(zhí)行遞歸操作,那么使用命名函數(shù)表達(dá)式的可能性就會(huì)大大增加。從這個(gè)意義上來(lái)說(shuō),理解命名函數(shù)表達(dá)式的語(yǔ)義及其 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);
  // 要么就退一步,使用沒(méi)有那么靈活的函數(shù)聲明
  function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
  }
  factorial(10);