DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 有名函數表達式全面解析(翻譯教程)
有名函數表達式全面解析(翻譯教程)
編輯:關於JavaScript     

網頁制作poluoluo文章簡介:簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在

簡介

Surprisingly, a topic of named function expressions doesn’t seem to be covered well enough on the web. This is probably why there are so many misconceptions floating around. In this article, I’ll try to summarize both - theoretical and practical aspects of these wonderful Javascript constructs; the good, bad and ugly parts of them.

在互聯網上很難找到一篇對有名函數表達式講述的比較全面的文章。這也可能是為什麼對此問題有那麼多的誤解的原因。在這篇文章中,我將試圖從理論和實踐兩個方面總結這些精彩的javascript結構,他的精華、雞肋和糟粕。

In a nutshell, named function expressions are useful for one thing only - descriptive function names in debuggers and profilers. Well, there is also a possibility of using function names for recursion, but you will soon see that this is often impractical nowadays. If you don’t care about debugging experience, you have nothing to worry about. Otherwise, read on to see some of the cross-browser glitches you would have to deal with and tips on how work around them.

簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在當下他通常不大實用。如果你不擔心調試的問題,那麼你就不用擔心了,甚至不去使用有名函數表達式。否則,繼續讀下去,你將看到你可能已經遇到的跨浏覽器問題和一些解決這些問題的方法。

I’ll start with a general explanation of what function expressions are how modern debuggers handle them. Feel free to skip to a final solution, which explains how to use these constructs safely.

下面我們先來介紹一下什麼是函數表達式,和現代的調試器如何處理他們呢。你也可以隨時跳轉到最後的解決方案,那裡將說明如何安全的使用這些結構

函數表達式和函數定義

One of the two most common ways to create a function object in ECMAScript is by means of either Function Expression or Function Declaration. The difference between two is rather confusing. At least it was to me. The only thing ECMA specs make clear is that Function Declaration must always have an Identifier (or a function name, if you prefer), and Function Expression may omit it:

在ECMAScript中有兩種簡單的創建函數對象的方法,一種是函數表達式,一種是函數聲明。兩者的區別讓人比較困惑,至少對我是這樣。ECMS規范中唯一澄清了的是函數聲明必須含有一個標示符(如果你喜歡,可以稱他為函數名),但是函數表達式可以省略他。

FunctionDeclaration :
function Identifier ( FormalParameterList opt ){ FunctionBody }

FunctionExpression :
function Identifier opt ( FormalParameterList opt ){ FunctionBody }

We can see that when identifier is omitted, that “something” can only be an expression. But what if identifier is present? How can one tell whether it is a function declaration or a function expression - they look identical after all? It appears that ECMAScript differentiates between two based on a context. If a function foo(){} is part of, say, assignment expression, it is considered a function expression. If, on the other hand, function foo(){} is contained in a function body or in a (top level of) program itself - it is parsed as a function declaration.

我們可以看到,當標示符被省略的時候,這樣的結構只能是一個函數表達式。但是當標示符出現的時候情況是怎樣的呢。他究竟是一個函數聲明還是一個函數表達式呢,他們看上去是完全一樣的。但是ECMAScript是通過上下文來區分兩者的。如果function foo(){}是一個賦值表達式的一部分,他就被認為是函數表達式。相反,如果function foo(){}包含在一個函數體中,或者在程序的最上層的代碼中,他就被解釋成為一個函數聲明。

  function foo(){}; // 聲明,因為作為最上層程序的一部分declaration, since it's part of a Program
  var bar = function foo(){}; // 表達式,因為是復制表達式的一部分expression, since it's part of an AssignmentExpression

  new function bar(){}; // 表達式,因為他是New表達式的一部分。expression, since it's part of a NewExpression

  (function(){
    function bar(){}; // 聲明,因為是函數圖的一部分declaration, since it's part of a FunctionBody
  })();

There’s a subtle difference in behavior of declarations and expressions.

聲明和表達式在作用上有這細微的差別。

First of all, function declarations are parsed and evaluated before any other expressions are. Even if declaration is positioned last in a source, it will be evaluated foremost any other expressions contained in a scope. The following example demonstrates how fn function is already defined by the time alert is executed, even though it’s being declared right after it:

首先,函數聲明是在所有表達式之前被解析和求值。即使聲明被放在代碼的最後,他也會在包含它的作用域內的所有表達式之前被求值。下面的例子將說明即使fn函數在alert執行之後才被聲明,但是在alert被執行的時候fn函數已經被定義了。

  alert(fn());

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

Another important trait of function declarations is that declaring them conditionally is non-standardized and varies across different environments. You should never rely on functions being declared conditionally and use function expressions instead.

函數聲明的另外一個重要的特點是,在條件語句中聲明他們的情況在ECMA標准中是沒有說明的,並且在不同的環境中(也就是不同的浏覽器)中是不同的。因此你不能依賴函數的條件聲明,而是應該使用函數表達式來代替。

  // 千萬不要這樣做Never do this!
  // 有些浏覽器會聲返回“first”的foo函數Some browsers will declare `foo` as the one returning 'first',
  // 有些浏覽器則會聲明返回“second”的foo函數while others - returning 'second'

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

  //作為替代你可以使用函數表達式 Instead, use function expressions:
  var foo;
  if (true) {
    foo = function() {
      return 'first';
    }
  }
  else {
    foo = function() {
      return 'second';
    }
  }
  foo();

Function expressions can actually be seen quite often. A common pattern in web development is to “fork” function definitions based on some kind of a feature test, allowing for the best performance. Since such forking usually happens in the same scope, it is almost always necessary to use function expressions. After all, as we know by now, function declarations should not be executed conditionally:

函數表達式可能更常見一些。在web開發中的一個普通的模式就是根據一些某種功能測試的情況來fork函數定義,允許達到最好的性能。既然這個fork通常發生在同一個作用域內,所以他需要使用函數表達式。畢竟,如我們所知,函數定義不應該被條件執行

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

有名函數表達式

Quite obviously, when a function expression has a name (technically - Identifier), it is called a named function expression. What you’ve seen in the very first example - var bar = function foo(){}; - was exactly that - a named function expression with foo being a function name. An important detail to remember is that this name is only available in the scope of a newly-defined function; specs mandate that an identifier should not be available to an enclosing scope:

非常顯然那,當一個函數表達式含有函數名(從技術上講就是一個標示符),他就被稱作有名函數表達式。在你看到的第一個例子中 var bar=function foo(){}就是這樣,他是一個以foo作為函數名的有名函數表達式。最重要的一個細節就是這個函數名只有在新定義的函數的作用域內才可訪問,ECMA要求一個標示符不應該被一個封閉的作用域所訪問

  var f = function foo(){
    return typeof foo; // “foo”在內部作用域可以被訪問 "foo" is available in this inner scope
  };
  //‘foo’在外部不可見 `foo` is never visible "outside"
  typeof foo; // "undefined"
  f(); // "function"

So what’s so special about these named function expressions? Why would we want to give them names at all?

It appears that named functions make for a much more pleasant debugging experience. When debugging an application, having a call stack with descriptive items makes a huge difference.

因此,為什麼這些有名函數表達式如此特別?為什麼我們想要給他們名字呢。

可以看到有命名的函數在調試過程中可能更加方便一些。當調試一個應用程序的時候擁有一個含有描述項的調用棧會有很大的不同

網頁制作poluoluo文章簡介:簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在

調試器中的函數名

When a function has a corresponding identifier, debuggers show that identifier as a function name, when inspecting call stack. Some debuggers (e.g. Firebug) helpfully show names of even anonymous functions - making them identical to names of variables that functions are assigned to. Unfortunately, these debuggers usually rely on simple parsing rules; Such extraction is usually quite fragile and often produces false results.

當一個函數有一個相應的標示符的時候,當檢查調用堆棧的時候調試器把這個標示符顯示為函數名。有些調試器(例如Firebug)將顯示甚至是匿名函數的名稱,使他們的名字和函數被賦值給的變量的名字一致。不幸的是,這些調試器通常依賴於簡單的解釋規則;這些提取出來的名稱通常是非常不穩定的,而且通常產生錯誤的結果

Let’s look at a simple example:

讓我們看一個簡單的例子

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

  // 這裡我們用函數聲明來定義所有3個函數Here, we used function declarations when defining all of 3 functions
  // 當調試器停止在`debugger`語句的時候When debugger stops at the `debugger` statement,
  //調用堆棧看上去非常具有描述性的 the call stack (in Firebug) looks quite descriptive:
  baz
  bar
  foo
  expr_test.html()

We can see that foo called bar which in its turn called baz (and that foo itself was called from the global scope of expr_test.html document). What’s really nice, is that Firebug manages to parse the “name” of a function even when an anonymous expression is used:

我們看以看到,foo調用bar,bar調用baz,foo本身是從expr_test.html的全局作用域中被調用的。Firebug非常突出的一點是即是使用一個匿名的表達式,他都試圖去解釋函數名

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

  // 調用堆棧Call stack
  baz
  bar()
  foo
  expr_test.html()

What’s not very nice, though, is that if a function expression gets any more complex (which, in real life, it almost always is) all of the debugger’s efforts turn out to be pretty useless; we end up with a shiny question mark in place of a function name

但是調試器還不是很完美,當函數表達式變得非常復雜的時候,這些調試器就很難去解釋這些函數名,通常是以一個問號來代替函數名

  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
  (?)()
  foo
  expr_test.html()

Another confusion appears when a function is being assigned to more than one variable:

另外一個令人迷惑的事情是,當一個函數被賦值給不止一個變量

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

  // Call stack:
  bar()
  foo
  expr_test.html()

You can see call stack showing that foo invoked bar. Clearly, that’s not what has happened. The confusion is due to the fact that baz was “exchanged” references with another function - the one alerting “spoofed”. As you can see, such parsing - while great in simple cases - is often useless in any non-trivial script.

你可以看到調用棧顯示foo調用bar。顯然,剛才發生地不是這樣。引起這種困惑是因為baz引用了另外一個函數---提示 “spoofed”的那個函數。就如你所看到的,這些解釋雖然在簡單的情況下非常有用,但是在復雜的腳本中卻基本上沒什麼用。

What it all boils down to is the fact that named function expressions is the only way to get a truly robust stack inspection. Let’s rewrite our previous example with named functions in mind. Notice how both of the functions returning from self-executing wrapper, are named as bar:

歸根結底,有名函數表達式實際上是得到真正的調用棧檢查的唯一方法。讓我們用有名函數表達式來重寫我們前面的例子。請注意,兩個從自執行包裹中返回的函數都有名為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();

  // And, once again, we have a descriptive call stack!
  baz
  bar
  foo
  expr_test.html()

Before we start dancing happily celebrating this holy grail finding, I’d like to bring a beloved JScript into the picture.

在我們開始跳舞慶祝找到這個方法之前,我們還是先來看看JScript中的情況是如何的

JScript bugs

Unfortunately, JScript (i.e. Internet Explorer’s ECMAScript implementation) seriously messed up named function expressions. JScript is responsible for named function expressions being recommended against by many people these days.

不幸的是,JScript(也就是EcMAScript的IE實現)處理有名函數表達式卻非常糟糕。JScript應對有名函數表達式不被很多人推薦負責

Let’s look at what exactly is wrong with its broken implementation. Understanding all of its issues will allow us to work around them safely. Note that I broke these discrepancies into few examples - for clarity - even though all of them are most likely a consequence of one major bug.

讓我們來看看這個ECMASCript的不好的實現中都有什麼問題。理解所有這些問題將使我們可以安全的使用它。雖然下面的這些例子都很可能是一個主要的bug的結果,但是為了清晰起見,我把這些差異分為了幾個例子。

網頁制作poluoluo文章簡介:簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在

Example #1: Function expression identifier leaks into an enclosing scope

實例1:函數表達式標示符滲進了外圍作用域

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

Remember how I mentioned that an identifier of named function expression is not available in an enclosing scope? Well, JScript doesn’t agree with specs on this one - g in the above example resolves to a function object. This is a most widely observed discrepancy. It’s dangerous in that it inadvertedly pollutes an enclosing scope - a scope that might as well be a global one - with an extra identifier. Such pollution can, of course, be a source of hard-to-track bugs.

我剛才提到過一個有名函數表達式的標示符不能在外部作用域中被訪問。但是,JScript在這點上和標准並不相符,在上面的餓例子中g卻是一個函數對象。這個是一個可以廣泛觀察到的差異。這樣它就用一個多余的標示符污染了外圍作用域,這個作用域很有可能是全局作用域,這樣是很危險的。當然這個污染可能是一個很難去處理和跟蹤的bug的根源

Example #2: Named function expression is treated as BOTH - function declaration AND function expression

實例2:有名函數表達式被進行了雙重處理,函數表達式和函數聲明

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

As I explained before, function declarations are parsed foremost any other expressions in a particular execution context. The above example demonstrates how JScript actually treats named function expressions as function declarations. You can see that it parses g before an “actual declaration” takes place.

正如我前面解釋的,在一個特定的執行環境中,函數聲明是在所有的表達式之前被解釋。上面的例子說明JScript實際上把有名函數表達式作為一個函數聲明來對待。我們可以看到他在一個實際的聲明之前就被解釋了。

This brings us to a next example:

在此基礎上我們引入了下面的一個例子。

Example #3: Named function expression creates TWO DISCTINCT function objects!

實例3:有名函數表達式創建兩個不同的函數對象。

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

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

This is where things are getting interesting. Or rather - completely nuts. Here we are seeing the dangers of having to deal with two distinct objects - augmenting one of them obviously does not modify the other one; This could be quite troublesome if you decided to employ, say, caching mechanism and store something in a property of f, then tried accessing it as a property of g, thinking that it is the same object you’re working with.

在這裡事情變得更加有趣了,或者是完全瘋掉。這裡我們看到必須處理兩個不同的對象的危險,當擴充他們當中的一個的時候,另外一個不會相應的改變。如果你打算使用cache機制並且在f的屬性中存放一些東西,只有有試圖在g的屬性中訪問,你本以為他們指向同一個對象,這樣就會變得非常麻煩

Let’s look at something a bit more complex.

讓我們來看一些更復雜的例子。

Example #4: Function declarations are parsed sequentially and are not affected by conditional blocks

實例4:函數聲明被順序的解釋,不受條件塊的影響

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

An example like this could cause even harder to track bugs. What happens here is actually quite simple. First, g is being parsed as a function declaration, and since declarations in JScript are independent of conditional blocks, g is being declared as a function from the “dead” if branch - function g(){ return 2 }. Then all of the “regular” expressions are being evaluated and f is being assigned another, newly created function object to. “dead” if branch is never entered when evaluating expressions, so f keeps referencing first function - function g(){ return 1 }. It should be clear by now, that if you’re not careful enough, and call g from within f, you’ll end up calling a completely unrelated g function object.

像這樣的一個例子可能會使跟蹤bug非常困難。這裡發生的問題卻非常簡單。首先g被解釋為一個函數聲明,並且既然JScript中的聲明是和條件塊無關的,g就作為來自於已經無效的if分支中的函數被聲明function g(){ return 2 }。之後普通的表達式被求值並且f被賦值為另外一個新創建的函數對象。當執行表達式的時候,由於if條件分支是不會被進入的,因此f保持為第一函數的引用 function g(){ return 1 }。現在清楚了如果不是很小心,而且在f內部調用g,你最終將調用一個完全無關的g函數對象。

You might be wondering how all this mess with different function objects compares to arguments.callee. Does callee reference f or g? Let’s take a look:

你可能在想不從的函數對象和arguments.callee相比較的結果會是怎樣呢?callee是引用f還是g?讓我們來看一下

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

As you can see, arguments.callee references same object as f identifier. This is actually good news, as you will see later on.

我們可以看到arguments.callee引用的是和f標示符一樣的對象,就像稍後你會看到的,這是個好消息

Looking at JScript deficiencies, it becomes pretty clear what exactly we need to avoid. First, we need to be aware of a leaking identifier (so that it doesn’t pollute enclosing scope). Second, we should never reference identifier used as a function name; A troublesome identifier is g from the previous examples. Notice how many ambiguities could have been avoided if we were to forget about g’s existance. Always referencing function via f or arguments.callee is the key here. If you use named expression, think of that name as something that’s only being used for debugging purposes. And finally, a bonus point is to always clean up an extraneous function created erroneously during NFE declaration.

既然看到了JScript的缺點,我們應該避免些什麼就非常清楚了。首先,我們要意識到標示符的滲出(以使得他不會污染外圍作用域)。第二點,我們不應該引用作為函數名的標示符;從前面的例子可以看出g是一個問題多多的標示符。請注意,如果我們忘記g的存在,很多歧義就可以被避免。通常最關鍵的就是通過f或者argument.callee來引用函數。如果你使用有名的表達式,記住名字只是為了調試的目的而存在。最後,額外的一點就是要經常清理有名函數表達式聲明錯誤創建的附加函數

I think last point needs a bit of an explanation:

我想最有一點需要一些更多解釋

JScript 內存管理

Being familiar with JScript discrepancies, we can now see a potential problem with memory consumption when using these buggy constructs. Let’s look at a simple example:

熟悉了JScript和規范的差別,我們可以看到當使用這些有問題的結構的時候,和內存消耗相關的潛在問題

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

We know that a function returned from within this anonymous invocation - the one that has g identifier - is being assigned to outer f. We also know that named function expressions produce superfluous function object, and that this object is not the same as returned function. The memory issue here is caused by this extraneous g function being literally “trapped” in a closure of returning function. This happens because inner function is declared in the same scope as that pesky g one. Unless we explicitly break reference to g function it will keep consuming memory.

我們發現從匿名調用中返回的一個函數,也就是以g作為標示符的函數,被復制給外部的f。我們還知道有名函數表達式創建了一個多余的函數對象,並且這個對象和返回的對象並不是同一個函數。這裡的內存問題就是由這個沒用的g函數在一個返回函數的閉包中被按照字面上的意思捕獲了。這是因為內部函數是和可惡的g函數在同一個作用域內聲明的。除非我們顯式的破壞到g函數的引用,否則他將一直占用內存。

  var f = (function(){
    var f, g;
    if (true) {
      f = function g(){};
    }
    else {
      f = function g(){};
    }
    //給g賦值null以使他不再被無關的函數引用。
        //null `g`, so that it doesn't reference extraneous function any longer
       
    g = null;
    return f;
  })();

Note that we explicitly declare g as well, so that g = null assignment wouldn’t create a global g variable in conforming clients (i.e. non-JScript ones). By nulling reference to g, we allow garbage collector to wipe off this implicitly created function object that g refers to.

注意,我們又顯式的聲明了g,所以g=null賦值將不會給符合規范的客戶端(例如非JScirpt引擎)創建一個全局變量。通過給g以null的引用,我們允許垃圾回收來清洗這個被g所引用的,隱式創建的函數對象。

When taking care of JScript NFE memory leak, I decided to run a simple series of tests to confirm that nulling g actually does free memory.

當考慮JScript的有名函數表達式的內存洩露問題時,我決定運行一系列簡單的測試來證實給g函數null的引用實際上可以釋放內存

網頁制作poluoluo文章簡介:簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在

測試

The test was simple. It would simply create 10000 functions via named function expressions and store them in an array. I would then wait for about a minute and check how high the memory consumption is. After that I would null-out the reference and repeat the procedure again. Here’s a test case I used:

這個測試非常簡單。他將通過有名函數表達式創建1000個函數,並將它們儲存在一個數組中。我等待了大約一分鐘,並查看內存使用有多高。只有我們加上null引用,重復上述過程。下面就是我使用的一個簡單的測試用例

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

Results as seen in Process Explorer on Windows XP SP2 were:

結果是在Windows XP SP2進行的,通過進程管理器得到的

  IE6:

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

  IE7:

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

The results somewhat confirmed my assumptions - explicitly nulling superfluous reference did free memory, but the difference in consumption was relatively insignificant. For 10000 function objects, there would be a ~3MB difference. This is definitely something that should be kept in mind when designing large-scale applications, applications that will run for either long time or on devices with limited memory (such as mobile devices). For any small script, the difference probably doesn’t matter.

結果在一定程度上證實了我的假設,顯示的給無用的參考以null值確實會釋放內存,但是在內寸的消耗的區別上貌似不是很大。對於1000個函數對象,大約應該有3M左右的差別。但是有一些是明確的,在設計大規模的應用的時候,應用要不就是要運行很長時間的或者要在一個內存有限的設備上(例如移動設備)。對於任何小的腳本,差別可能不是很重要。

You might think that it’s all finally over, but we are not just quite there yet :) There’s a tiny little detail that I’d like to mention and that detail is Safari 2.x

你可以認為這樣就可以結束了,但是還沒到結束的時候。我還要討論一些小的細節,而且這些細節是在Safari 2.x下的

Safari bug

Even less widely known bug with NFE is present in older versions of Safari; namely, Safari 2.x series. I’ve seen some claims on the web that Safari 2.x does not support NFE at all. This is not true. Safari does support it, but has bugs in its implementation which you will see shortly.

雖然沒有被人們發現在早期的Safari版本,也就是Safari 2.x版本中有名函數表達式的bug。但是我在web上看到一些聲稱Safari 2.x根本不支持有名函數表達式。這不是真的。Safari的確支持有名函數表達式,但是稍後你將看到在它的實現中是存在bug的

When encountering function expression in a certain context, Safari 2.x fails to parse the program entirely. It doesn’t throw any errors (such as SyntaxError ones). It simply bails out:

在某些執行環境中遇到函數表達式的時候,Safari 2.x 將解釋程序整體失敗。它不拋出任何的錯誤(例如SyntaxError)。展示如下

  (function f(){})(); // <== 有名函數表達式 NFE
  alert(1); //因為前面的表達式是的整個程序失敗,本行將無法達到, this line is never reached, since previous expression fails the entire program

After fiddling with various test cases, I came to conclusion that Safari 2.x fails to parse named function expressions, if those are not part of assignment expressions. Some examples of assignment expressions are:

在用一些測試用例測試之後,我總結出,如果有名函數表達式不是賦值表達式的一部分,Safari解釋有名函數表達式將失敗。一些賦值表達式的例子如下

  // 變量聲明part of variable declaration 
    var f = 1;

    //簡單的賦值 part of simple assignment
    f = 2, g = 3;

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

This means that putting named function expression into an assignment makes Safari “happy”:

這就意味著把有名函數表達式放到賦值表達式中會讓 Safari非常“開心”

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

  var f = function f(){}; // works 成功
 
  (function(){
    return function f(){}; // fails 失敗
  })();

  (function(){
    return (f = function f(){}); // works 成功
  })();

  setTimeout(function f(){ }, 100); // fails

It also means that we can’t use such common pattern as returning named function expression without an assignment:

這也意味著我們不能使用這種普通的模式而沒有賦值表達式作為返回有名函數表達式


  //要取代這種Safari2.x不兼容的情況 Instead of this non-Safari-2x-compatible syntax:
  (function(){
    if (featureTest) {
      return function f(){};
    }
    return function f(){};
  })();

  // 我們應該使用這種稍微冗長的替代方法we should use this slightly more verbose alternative:
  (function(){
    var f;
    if (featureTest) {
      f = function f(){};
    }
    else {
      f = function f(){};
    }
    return f;
  })();

  // 或者另外一種變形or another variation of it:
  (function(){
    var f;
    if (featureTest) {
      return (f = function f(){});
    }
    return (f = function f(){});
  })();

  /*
    Unfortunately, by doing so, we introduce an extra reference to a function
    which gets trapped in a closure of returning function. To prevent extra memory usage,
    we can assign all named function expressions to one single variable.
                不幸的是 這樣做我們引入了對函數的另外一個引用
                他將被包含在返回函數的閉包中
                為了防止多於的內存使用,我們可以吧所有的有名函數表達式賦值給一個單獨的變量
  */

  var __temp;

  (function(){
    if (featureTest) {
      return (__temp = function f(){});
    }
    return (__temp = function f(){});
  })();

  ...

  (function(){
    if (featureTest2) {
      return (__temp = function g(){});
    }
    return (__temp = function g(){});
  })();

  /*
    Note that subsequent assignments destroy previous references,
    preventing any excessive memory usage.
                注釋:後面的賦值銷毀了前面的引用,防止任何過多的內存使用
  */

If Safari 2.x compatibility is important, we need to make sure “incompatible” constructs do not even appear in the source. This is of course quite irritating, but is definitely possible to achieve, especially when knowing the root of the problem.

如果Safari2.x的兼容性非常重要。我們需要保證不兼容的結構不再代碼中出現。這當然是非常氣人的,但是他確實明確的可以做到的,尤其是當我們知道問題的根源。

It’s also worth mentioning that declaring a function as NFE in Safari 2.x exhibits another minor glitch, where function representation does not contain function identifier:

還值得一提的是在Safari中聲明一個函數是有名函數表達式的時候存在另外一個小的問題,這是函數表示法不含有函數標示符(估計是toString的問題)

  var f = function g(){};

  // Notice how function representation is lacking `g` identifier
  String(g); // function () { }

This is not really a big deal. As I have already mentioned before, function decompilation is something that should not be relied upon anyway.

這不是個很大的問題。因為之前我已經說過,函數反編譯在任何情況下都是不可信賴的。

解決方案

  var fn = (function(){

    //聲明一個變量,來給他賦值函數對象 declare a variable to assign function object to
    var f;

    // 條件的創建一個有名函數 conditionally create a named function
    // 並把它的引用賦值給f and assign its reference to `f`
    if (true) {
      f = function F(){ }
    }
    else if (false) {
      f = function F(){ }
    }
    else {
      f = function F(){ }
    }

    //給一個和函數名相關的變量以null值 Assign `null` to a variable corresponding to a function name
    //這可以使得函數對象(通過標示符的引用)可以被垃圾收集所得到This marks the function object (referred to by that identifier)
    // available for garbage collection
    var F = null;

    //返回一個條件定義的函數 return a conditionally defined function
    return f;
  })();

Finally, here’s how we would apply this “techinque” in real life, when writing something like a cross-browser addEvent function:

最後,當我麼一個類似於跨浏覽器addEvent函數的類似函數時,下面就是我們如何在真實的應用中使用這個技術


  // 1) 用一個分離的作用域封裝聲明 enclose declaration with a separate scope
  var addEvent = (function(){

    var docEl = document.documentElement;

    // 2)聲明一個變量,用來賦值為函數  declare a variable to assign function to
    var fn;

    if (docEl.addEventListener) {

      // 3) 確保給函數一個描述的標示符 make sure to give function a descriptive identifier
      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創建的addEvent函數  clean up `addEvent` function created by JScript
    //    保證在賦值之前加上varmake sure to either prepend assignment with `var`,
    //    或者在函數頂端聲明 addEvent or declare `addEvent` at the top of the function
    var addEvent = null;

    // 5)最後通過fn返回函數的引用 finally return function referenced by `fn`
    return fn;
  })();

網頁制作poluoluo文章簡介:簡而言之,有名函數表達式只對一件工作有用,在調試器和性能測試器中描述函數的名稱。當然還有可能在遞歸調用中使用函數名稱,但是你將很快看到在

可替代的解決方案

It’s worth mentioning that there actually exist alternative ways of having descriptive names in call stacks. Ways that don’t require one to use named function expressions. First of all, it is often possible to define function via declaration, rather than via expression. This option is only viable when you don’t need to create more than one function:

需要說明,實際上純在一個種使得在調用棧上顯示描述名稱(函數名)的替代方法。一個不需要使用有名函數表達式的方法。首先,通常可以使用聲明而不是使用表達式來定義函數。這種選擇通常只是適應於你不需要創建多個函數的情況。

  var hasClassName = (function(){

    // 定義一些私有變量define some private variables
    var cache = { };

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

    // 返回函數return function
    return hasClassName;
  })();

This obviously wouldn’t work when forking function definitions. Nevertheless, there’s an interesting pattern that I first seen used by Tobie Langel. The way it works is by defining all functions upfront using function declarations, but giving them slightly different identifiers:

這種方法顯然對於多路的函數定義不適用。但是,有一個有趣的方法,這個方法我第一次在看到Tobie Langel.在使用。這個用函數聲明定義所有的函數,但是給這個函數聲明以稍微不同的標示符。

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

While it’s an elegant approach, it has its own drawbacks. First, by using different identifiers, you loose naming consistency. Whether it’s good or bad thing is not very clear. Some might prefer to have identical names, while others wouldn’t mind varying ones; after all, different names can often “speak” about implementation used. For example, seeing “attachEvent” in debugger, would let you know that it is an attachEvent-based implementation of addEvent. On the other hand, implementation-related name might not be meaningful at all. If you’re providing an API and name “inner” functions in such way, the user of API could easily get lost in all of these implementation details.

雖然這是一個比較優雅的方法,但是他也有自己的缺陷。首先,通過使用不同的標示符,你失去的命名的一致性。這是件好的事情還是件壞的事情還不好說。有些人希望使用一支的命名,有些人則不會介意改變名字;畢竟,不同的名字通常代表不同的實現。例如,在調試器中看到“attachEvent”,你就可以知道是addEvent基於attentEvent的一個實現。另外一方面,和實現相關的名字可能根本沒有什意義。如果你提供一個api並用如此方法命名內部的函數,api的使用者可能會被這些實現細節搞糊塗。

A solution to this problem might be to employ different naming convention. Just be careful not to introduce extra verbosity. Some alternatives that come to mind are:

解決這個問題的一個方法是使用不同的命名規則。但是注意不要飲用過多的冗余。下面列出了一些替代的命名方法

  `addEvent`, `altAddEvent` and `fallbackAddEvent`
  // or
  `addEvent`, `addEvent2`, `addEvent3`
  // or
  `addEvent_addEventListener`, `addEvent_attachEvent`, `addEvent_asProperty`

Another minor issue with this pattern is increased memory consumption. By defining all of the function variations upfront, you implicitly create N-1 unused functions. As you can see, if attachEvent is found in document.documentElement, then neither addEventListener nor addEventAsProperty are ever really used. Yet, they already consume memory; memory which is never deallocated for the same reason as with JScript’s buggy named expressions - both functions are “trapped” in a closure of returning one.

這種模式的另外一個問題就是增加了內存的開銷。通過定義所有上面的函數變種,你隱含的創建了N-1個函數。你可以發現,如果attachEvent在document.documentElement中發現,那麼addEventListener和addEventAsProperty都沒有被實際用到。但是他們已經消耗的內存;和Jscript有名表達式bug的原因一樣的內存沒有被釋放,在返回一個函數的同時,兩個函數被‘trapped‘在閉包中。

This increased consumption is of course hardly an issue. If a library such as Prototype.js was to use this pattern, there would be not more than 100-200 extra function objects created. As long as functions are not created in such way repeatedly (at runtime) but only once (at load time), you probably shouldn’t worry about it.

這個遞增的內存使用顯然是個嚴重的問題。如果和Prototype.js類似的庫需要使用這種模式,將有另外的100-200個多於的函數對象被創建。如果函數沒有被重復地(運行時)用這種方式創建,只是在加載時被創建一次,你可能就不用擔心這個問題。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved