<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解JavaScript之揭秘命名函數(shù)表達式

          共 14918字,需瀏覽 30分鐘

           ·

          2021-03-16 11:22

          來源 | 看云  

          英文原文 | http://kangax.github.com/nfe/

          前言

          本文將從原理和實踐兩個方面來探討JavaScript關(guān)于命名函數(shù)表達式的優(yōu)缺點。
          簡單的說,命名函數(shù)表達式只有一個用戶,那就是在Debug或者Profiler分析的時候來描述函數(shù)的名稱,也可以使用函數(shù)名實現(xiàn)遞歸,但很快你就會發(fā)現(xiàn)其實是不切實際的。
          當然,如果你不關(guān)注調(diào)試,那就沒什么可擔心的了,否則,如果你想了解兼容性方面的東西的話,你還是應該繼續(xù)往下看看。
          我們先開始看看,什么叫函數(shù)表達式,然后再說一下現(xiàn)代調(diào)試器如何處理這些表達式,如果你已經(jīng)對這方面很熟悉的話,請直接跳過此小節(jié)。

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

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

          函數(shù)聲明:

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

          函數(shù)表達式:

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

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

          function foo(){} // 聲明,因為它是程序的一部分var bar = function foo(){}; // 表達式,因為它是賦值表達式的一部分
          new function bar(){}; // 表達式,因為它是new表達式
          (function(){function bar(){} // 聲明,因為它是函數(shù)體的一部分})();

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

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

          你可以會想到,在使用eval對JSON進行執(zhí)行的時候,JSON字符串通常被包含在一個圓括號里:eval('(' + json + ')'),這樣做的原因就是因為分組操作符,也就是這對括號,會讓解析器強制將JSON的花括號解析成表達式而不是代碼塊。

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

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

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

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

          // 千萬別這樣做!// 因為有的瀏覽器會返回first的這個function,而有的瀏覽器返回的卻是第二個
          if (true) {function foo() { return 'first';}}else {function foo() { return 'second';}}foo();
          // 相反,這樣情況,我們要用函數(shù)表達式var foo;if (true) {foo = function() { return 'first';};}else {foo = function() { return 'second';};}foo();

          函數(shù)聲明的實際規(guī)則如下:

          _函數(shù)聲明_只能出現(xiàn)在_程序_或_函數(shù)體_內(nèi)。從句法上講,它們 不能出現(xiàn)在Block(塊)({ ... })中,例如不能出現(xiàn)在 if、while 或 for 語句中。

          因為 Block(塊) 中只能包含Statement語句, 而不能包含_函數(shù)聲明_這樣的源元素。另一方面,仔細看一看規(guī)則也會發(fā)現(xiàn),唯一可能讓_表達式_出現(xiàn)在Block(塊)中情形,就是讓它作為_表達式語句_的一部分。

          但是,規(guī)范明確規(guī)定了_表達式語句_不能以關(guān)鍵字function開頭。

          而這實際上就是說,_函數(shù)表達式_同樣也不能出現(xiàn)在Statement語句或Block(塊)中(因為Block(塊)就是由Statement語句構(gòu)成的)。

          函數(shù)語句

          在ECMAScript的語法擴展中,有一個是函數(shù)語句,目前只有基于Gecko的瀏覽器實現(xiàn)了該擴展,所以對于下面的例子,我們僅是抱著學習的目的來看,一般來說不推薦使用(除非你針對Gecko瀏覽器進行開發(fā))。

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

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

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

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

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

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

          不過,我們可以使用下面這樣的符合標準的代碼來模式上面例子中的函數(shù)語句:

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

          4、函數(shù)語句和函數(shù)聲明(或命名函數(shù)表達式)的字符串表示類似,也包括標識符:

          if (true) {function foo(){ return 1; }}String(foo); // function foo() { 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(); // FF3以下返回1,F(xiàn)F3.5以上返回2
          // 不過,如果前面是函數(shù)表達式,則沒用問題var foo = function(){ return 1; };if (true) {function foo(){ return 2; }}foo(); // 所有版本都返回2

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

          命名函數(shù)表達式

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

          // 該代碼來自Garrett Smith的APE Javascript library庫(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ù)表達式,理所當然,就是它得有名字,前面的例子var bar = function foo(){};就是一個有效的命名函數(shù)表達式,但有一點需要記住:這個名字只在新定義的函數(shù)作用域內(nèi)有效,因為規(guī)范規(guī)定了標示符不能在外圍的作用域內(nèi)有效:

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

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

          正如我們開頭所說:給它一個名字就是可以讓調(diào)試過程更方便,因為在調(diào)試的時候,如果在調(diào)用棧中的每個項都有自己的名字來描述,那么調(diào)試過程就太爽了,感受不一樣嘛。

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

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

          function foo(){return bar();}function bar(){return baz();}function baz(){debugger;}foo();
          // 這里我們使用了3個帶名字的函數(shù)聲明// 所以當調(diào)試器走到debugger語句的時候,F(xiàn)irebug的調(diào)用棧上看起來非常清晰明了 // 因為很明白地顯示了名稱bazbarfooexpr_test.html()

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

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

          然后,當函數(shù)表達式稍微復雜一些的時候,調(diào)試器就不那么聰明了,我們只能在調(dià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 stackbaz(?)() // 這里可是問號哦fooexpr_test.html()

          另外,當把函數(shù)賦值給多個變量的時候,也會出現(xiàn)令人郁悶的問題:

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

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

          歸根結(jié)底,只有給函數(shù)表達式取個名字,才是最委托的辦法,也就是使用命名函數(shù)表達式。

          我們來使用帶名字的表達式來重寫上面的例子(注意立即調(diào)用的表達式塊里返回的2個函數(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)用棧信息了耶!bazbarfooexpr_test.html()

          OK,又學了一招吧?不過在高興之前,我們再看看不同尋常的JScript吧。

          JScript的Bug

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

          下面我們就來看看IE在實現(xiàn)中究竟犯了那些錯誤,俗話說知已知彼,才能百戰(zhàn)不殆。我們來看看如下幾個例子:

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

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

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

          注:IE9貌似已經(jīng)修復了這個問題

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

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

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

          這個例子引出了下一個例子。
          例3:命名函數(shù)表達式會創(chuàng)建兩個截然不同的函數(shù)對象!

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

          看到這里,大家會覺得問題嚴重了,因為修改任何一個對象,另外一個沒有什么改變,這太惡了。

          通過這個例子可以發(fā)現(xiàn),創(chuàng)建2個不同的對象,也就是說如果你想修改f的屬性中保存某個信息,然后想當然地通過引用相同對象的g的同名屬性來使用,那問題就大了,因為根本就不可能。

          再來看一個稍微復雜的例子:

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

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

          這個bug查找就難多了,但導致bug的原因卻非常簡單。首先,g被當作函數(shù)聲明解析,由于JScript中的函數(shù)聲明不受條件代碼塊約束,所以在這個很惡的if分支中,g被當作另一個函數(shù)function g(){ return 2 },也就是又被聲明了一次。

          然后,所有“常規(guī)的”表達式被求值,而此時f被賦予了另一個新創(chuàng)建的對象的引用。

          由于在對表達式求值的時候,永遠不會進入“這個可惡if分支,因此f就會繼續(xù)引用第一個函數(shù)function g(){ return 1 }。

          分析到這里,問題就很清楚了:假如你不夠細心,在f中調(diào)用了g,那么將會調(diào)用一個毫不相干的g函數(shù)對象。

          你可能會文,將不同的對象和arguments.callee相比較時,有什么樣的區(qū)別呢?我們來看看:

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

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

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

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

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

          了解了JScript這么變態(tài)以后,我們就要及時預防這些問題了,首先防范標識符泄漏帶外部作用域,其次,應該永遠不引用被用作函數(shù)名稱的標識符;還記得前面例子中那個討人厭的標識符g嗎?——如果我們能夠當g不存在,可以避免多少不必要的麻煩哪。

          因此,關(guān)鍵就在于始終要通過f或者arguments.callee來引用函數(shù)。如果你使用了命名函數(shù)表達式,那么應該只在調(diào)試的時候利用那個名字。最后,還要記住一點,一定要把命名函數(shù)表達式聲明期間錯誤創(chuàng)建的函數(shù)清理干凈。

          對于,上面最后一點,我們還得再解釋一下。

          JScript的內(nèi)存管理

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

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

          我們知道,這個匿名函數(shù)調(diào)用返回的函數(shù)(帶有標識符g的函數(shù)),然后賦值給了外部的f。

          我們也知道,命名函數(shù)表達式會導致產(chǎn)生多余的函數(shù)對象,而該對象與返回的函數(shù)對象不是一回事。

          所以這個多余的g函數(shù)就死在了返回函數(shù)的閉包中了,因此內(nèi)存問題就出現(xiàn)了。這是因為if語句內(nèi)部的函數(shù)與g是在同一個作用域中被聲明的。

          這種情況下 ,除非我們顯式斷開對g函數(shù)的引用,否則它一直占著內(nèi)存不放。

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

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

          測試

          測試很簡單,就是命名函數(shù)表達式創(chuàng)建10000個函數(shù),然后把它們保存在一個數(shù)組中。等一會兒以后再看這些函數(shù)到底占用了多少內(nèi)存。然后,再斷開這些引用并重復這一過程。下面是測試代碼:

          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();}

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

           IE6:
          without `null`: 7.6K -> 20.3K with `null`: 7.6K -> 18K
          IE7:
          without `null`: 14K -> 29.7K with `null`: 14K -> 27K

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

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

          SpiderMonkey的怪癖

          大家都知道,命名函數(shù)表達式的標識符只在函數(shù)的局部作用域中有效。但包含這個標識符的局部作用域又是什么樣子的嗎?其實非常簡單。

          在命名函數(shù)表達式被求值時,會創(chuàng)建一個特殊的對象,該對象的唯一目的就是保存一個屬性,而這個屬性的名字對應著函數(shù)標識符,屬性的值對應著那個函數(shù)。這個對象會被注入到當前作用域鏈的前端。然后,被“擴展”的作用域鏈又被用于初始化函數(shù)。

          在這里,有一點十分有意思,那就是ECMA-262定義這個(保存函數(shù)標識符的)“特殊”對象的方式。

          標準說**“像調(diào)用new Object()表達式那樣”**創(chuàng)建這個對象。如果從字面上來理解這句話,那么這個對象就應該是全局Object的一個實例。

          然而,只有一個實現(xiàn)是按照標準字面上的要求這么做的,這個實現(xiàn)就是SpiderMonkey。因此,在SpiderMonkey中,擴展Object.prototype有可能會干擾函數(shù)的局部作用域:

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

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

          另一個把內(nèi)部對象實現(xiàn)為全局Object對象的是黑莓(Blackberry)瀏覽器。目前,它的_活動對象_(Activation Object)仍然繼承Object.prototype。

          可是,ECMA-262并沒有說_活動對象_也要“像調(diào)用new Object()表達式那樣”來創(chuàng)建(或者說像創(chuàng)建保存NFE標識符的對象一樣創(chuàng)建)。人家規(guī)范只說了_活動對象_是規(guī)范中的一種機制。

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

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

          不過神奇的還是,函數(shù)中的變量甚至會與已有的Object.prototype的成員發(fā)生沖突,來看看下面的代碼:

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

          要避免這個問題,要避免使用Object.prototype里的屬性名稱,如toString, valueOf, hasOwnProperty等等。

          JScript解決方案

          var fn = (function(){
          // 聲明要引用函數(shù)的變量var f;
          // 有條件地創(chuàng)建命名函數(shù)// 并將其引用賦值給fif (true) { f = function F(){ }}else if (false) { f = function F(){ }}else { f = function F(){ }}
          // 聲明一個與函數(shù)名(標識符)對應的變量,并賦值為null// 這實際上是給相應標識符引用的函數(shù)對象作了一個標記,// 以便垃圾回收器知道可以回收它了var F = null;
          // 返回根據(jù)條件定義的函數(shù)return f;})();

          最后我們給出一個應用上述技術(shù)的應用實例,這是一個跨瀏覽器的addEvent函數(shù)代碼:

          // 1) 使用獨立的作用域包含聲明var addEvent = (function(){
          var docEl = document.documentElement;
          // 2) 聲明要引用函數(shù)的變量var fn;
          if (docEl.addEventListener) {
          // 3) 有意給函數(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)聲明了addEventvar addEvent = null;
          // 5) 最后返回由fn引用的函數(shù)return fn;})();

          替代方案

          其實,如果我們不想要這個描述性名字的話,我們就可以用最簡單的形式來做,也就是在函數(shù)內(nèi)部聲明一個函數(shù)(而不是函數(shù)表達式),然后返回該函數(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;})();

          顯然,當存在多個分支函數(shù)定義時,這個方案就不行了。不過有種模式貌似可以實現(xiàn):那就是提前使用函數(shù)聲明來定義所有函數(shù),并分別為這些函數(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;})();

          雖然這個方案很優(yōu)雅,但也不是沒有缺點。第一,由于使用不同的標識符,導致喪失了命名的一致性。且不說這樣好還是壞,最起碼它不夠清晰。

          有人喜歡使用相同的名字,但也有人根本不在乎字眼上的差別。可畢竟,不同的名字會讓人聯(lián)想到所用的不同實現(xiàn)。

          例如,在調(diào)試器中看到attachEvent,我們就知 道addEvent是基于attachEvent的實現(xiàn)。當 然,基于實現(xiàn)來命名的方式也不一定都行得通。假如我們要提供一個API,并按照這種方式把函數(shù)命名為inner。

          那么API用戶的很容易就會被相應實現(xiàn)的 細節(jié)搞得暈頭轉(zhuǎn)向。

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

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

          另外,這種模式還存在一個小問題,即增加內(nèi)存占用。提前創(chuàng)建N個不同名字的函數(shù),等于有N-1的函數(shù)是用不到的。

          具體來講,如果document.documentElement 中包含attachEvent,那么addEventListener 和addEventAsProperty則根本就用不著了。

          可是,他們都占著內(nèi)存哪;而且,這些內(nèi)存將永遠都得不到釋放,原因跟JScript臭哄哄的命名表達式相同——這兩個函數(shù)都被“截留”在返回的那個函數(shù)的閉包中了。

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

          WebKit的displayName

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

          未來考慮

          將來的ECMAScript-262第5版(目前還是草案)會引入所謂的嚴格模式(strict mode)。

          開啟嚴格模式的實現(xiàn)會禁用語言中的那些不穩(wěn)定、不可靠和不安全的特性。據(jù)說出于安全方面的考慮,arguments.callee屬性將在嚴格模式下被“封殺”。因此,在處于嚴格模式時,訪問arguments.callee會導致TypeError(參見ECMA-262第5版的10.6節(jié))。

          而我之所以在此提到嚴格模式,是因為如果在基于第5版標準的實現(xiàn)中無法使用arguments.callee來執(zhí)行遞歸操作,那么使用命名函數(shù)表達式的可能性就會大大增加。從這個意義上來說,理解命名函數(shù)表達式的語義及其bug也就顯得更加重要了。

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

          致謝

          理查德· 康福德(Richard Cornford),是他率先解釋了JScript中命名函數(shù)表達式所存在的bug。理查德解釋了我在這篇文章中提及的大多數(shù)bug,所以我強烈建議大家去看看他的解釋。我還要感謝**Yann-Erwan Perio和道格拉斯·克勞克佛德(Douglas Crockford)**,他們早在2003年就在comp.lang.javascript論壇中提及并討論NFE問題了。

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

          托比·蘭吉**的點子被我用在了“替代方案”中。

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


          本文完~

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  激情性爱av | 性爱基地| 操骚逼自拍 | 无码区精品区一区二区三区 | 91精品国产91久久久久久久久久 |