命名函数表达式探秘

简介

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.

因此,为什么这些有名函数表达式如此特别?为什么我们想要给他们名字呢。

可以看到有命名的函数在调试过程中可能更加方便一些。当调试一个应用程序的时候拥有一个含有描述项的调用栈会有很大的不同

调试器中的函数名

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的结果,但是为了清晰起见,我把这些差异分为了几个例子。

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的引用实际上可以释放内存

测试

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

可替代的解决方案

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个多于的函数对象被创建。如果函数没有被重复地(运行时)用这种方式创建,只是在加载时被创建一次,你可能就不用担心这个问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值