JavaScript的各种函数定义

今天正好有机会,和实习的一个童鞋讨论这个问题的时候,正好也对自己之前的困惑有了更好的解答,从MDC这篇文档(Functions and function scope)中搞清楚不少,记录下来,认真总结总结。

话说刚刚提到那片文档的内涵确实很丰富,不过今天着重要了解的是关于函数定义这块的。首先看一下JavaScript最常见的四种函数定义:

  1. 用Function构造函数定义的函数,代码如下:
    var multiply = new Function('x', 'y', 'return x * y;');
  2. 函数声明,这种方式也是最为常见的一种:
    function multiply(x, y) {
        return x * y;
    }
  3. 函数表达式,声明为匿名函数然后赋值给一变量,很常见的方式:
    var multiply = function(x, y) {
        return x * y;
    }
  4. 函数表达式,但是函数声明为命名函数再赋值给一变量,长得跟上一种方式真像:
    var multiply = function multi(x, y) {
        return x * y;
    }

首先比较一下函数名,以及将函数赋值给的那个函数变量直接的关系,真绕……直观一点儿,从刚刚的例4说吧,就是multiply这个函数变量与multi这个函数名的关系:

  • 函数名是不能够被修改的,相反的,函数变量是可以重新被赋值的。函数变量可以被重新赋值应该很好理解,我们第4个例子刚刚定义的multiply这个变量,看它不顺眼,重新赋值为:
    multiply = function(x, y) {
       return x + y;
    }

    立马摇身一变,从乘法变成加法了。但是multi这个函数变量想变就是不可能的了,函数定义已经在那儿了,只要还保留这它的引用,它就是不会变的,可能这里不大好理解,先这样想着,往下看,慢慢就应该能理解了。

  • 函数名同时是无法在函数外部使用的,它只在函数体内部可见,一个很简单的例子:
    var foo = function bar() {
        alert('hello');
    }
    foo(); // 提示“hello”字符串
    bar(); // 执行报错,bar未定义

    和明显,这里的bar确实是一个函数名,但是它确实不能在外部调用。这时候肯定会有童鞋问干嘛这个例子还是长得那么乖,和例4一个样,怎么不用例2的方式呢?问得好,且听我慢慢分解。

  • 继续说例4,我们可以看见函数名(multi)函数变量(multiply),本不相同,其实两者根本就没有任何关系,因此没有保持一致的必要。说到这儿,我想上面4个例子应该可以精简到3个,例2和例4本质应该是一致的。什么,不信?嘻嘻,我还得继续卖关子哈~继续读下去~~

我们发现例2和例4相比,只不过少了var的函数变量,而例3与例4相比,只不过少了那个函数名,这里从现象上看,例2和例4的本质是相同的,铁证如下:

function foo() {}
alert(foo); // 提示包含“foo”的函数名
var bar = foo;
alert(bar); // 提示依然只包含“foo”的函数名,和bar半毛钱关系都没有

的确是铁证吧?上面的类似例2的代码结合起来写是不是就成例4的方式了?正确,这就是我刚刚所说的两者本质应该相同,只是用例2方式定义函数的时候,JS引擎帮我们做了一些事情,比如声明了函数名为multiply的函数,同时还悄悄定义了一个也叫multiply的变量,然后赋值给这个变量,两个完全一样的名字,我们自以为在使用函数名multiply的时候,实际是在用multiply这个函数变量,晕了吧~说实话,我也晕了~~总之我们调用的时候,实在用函数变量调用,而函数名是无法在外部调用函数的,因此有了我上述的推断。

但是这里要提到的一个小小的差别,函数声明方式定义的函数,与构造函数声明的或者函数表达式声明的不同之处在于,函数声明方式的函数可以在函数定义之前就调用……不说了,还是看代码:

foo(); // 提示Foo
function foo() {
    alert('Foo');
}
bar(); // 哥们,和上面确实不一样,就不要逞能,这不报错了?提示bar未定义
var bar = function() {
    alert('Bar');
}

再说说构造函数声明的函数,这样声明的函数是不会继承当前声明位置的作用域,它默认只会拥有全局作用域,然而这个是其他几种函数声明方式也一样有的,如下:

function foo() {
    var hi = 'hello';
    //return function() {
    //    alert(hi);
    //};
    return Function('return hi;');
}
foo()(); // 执行效果大家自己跑一下看看

可以想见,用构造函数声明返回的这个函数执行必然报错,因为其作用域(即全局作用域)中没有hi这个变量。

还有一点,就是往往大家要说构造函数方式声明的函数效率要低,这是为什么呢?今天从文档是得知是因为另外3种方式申明的函数只会被解析一次,其实他们存在于闭包中,但是那也只与作用域链有关,函数体是只会被解析一次的。但是构造函数方式呢,每次执行函数的时候,其函数体都会被解析一次,我们可以想想这样声明的函数是一个对象,其中存放了参数以及函数体,每次执行的时候都要先解析一次,参数和函数体,才会执行,这样必然效率低下。具体实验不知道如何做?

最后说一个大家都不怎么注意的地方,什么时候看似函数声明方式的方式却不是函数生命方式(还是这么绕~简单点儿说,就是例2的方式什么时候在不经意间就成其他方式了):

  • 当成为表达式的一部分,就如同例3和例4。
  • 不再是脚本本身或者函数的“源元素”(source element)。什么是源元素呢?即在脚本中的非嵌套语句或者函数体(A "source element" is a non-nested statement in the script or a function body),例如:
    var x = 0;               // source element  
    if (x == 0) {            // source element  
       x = 10;               // not a source element, 因为嵌套在了if语句里
       function boo() {}     // not a source element, 因为嵌套在了if语句里
    }  
    function foo() {         // source element  
       var y = 20;           // source element  
       function bar() {}     // source element  
       while (y == 10) {     // source element  
          function blah() {} // not a source element, 因为嵌套在了while语句里
          y++;               // not a source element, 因为嵌套在了while语句里
       }  
    }  

    源元素的概念大概有了理解,继续刚刚说的函数声明,请看:

    // 函数声明
    function foo() {}  
      
    // 函数表达式
    (function bar() {})  
      
    // 函数表达式
    x = function hello() {}  
    
    if (x) {  
       // 函数表达式
       function world() {}  
    }
    
    // function statement  
    function a() {  
       // function statement  
       function b() {}  
       if (0) {  
          // 函数表达式
          function c() {}  
       }  
    }
    

最后这里说一下我自己的理解,之所以要区分函数声明与非函数声明,因为在我看了,函数声明方式的函数定义,在JS解析引擎执行的时候会将其提前声明,也就是像我们刚刚上面说的那样,可以在函数定义之前使用,实际上是解析引擎在我们使用前已经将其解析了,但是非函数声明式,就像表达式函数声明,JS解析引擎只会把var声明的变量提前定义,此时变量值为undefined,而真正对这个变量的赋值是在代码实际所在位置,因此上述提到报错都是undefined,实际变量已经定义了,只是还没有赋值,JS解析引擎不知道它为函数。

JavaScript中的函数概念真是博大精深啊,特别是再与对象挂起钩来,真是乱啊,援引圆心的一篇文章:与 Function 和 Object 相关的有趣代码 这其中的东西可真是太丰富了。

以上文字都是自己的理解+阅读文档得来,如果有什么不对,或者不恰当,或者不完整,或者……还请大家雅正,谢谢!

update: 2010-09-11 经过玉伯提醒,发现source element是mozilla自己的概念,其他JS引擎没有类似的规范,最后感谢玉伯给了一篇精彩的文章,Named function expressions demystified介绍我之前提到的这些,内容十分丰富,需要慢慢阅读。最后再次感叹一下,前端这潭水真的太深了~

4 Comments For This Post

  1. 确实要注意IE和非IE浏览器的区别,这段代码在IE下是没报错的。。。
    var foo = function bar() {
    alert('hello');
    }
    foo(); // 提示“hello”字符串
    bar(); // IE不报错

    回复该留言

  2. 爱死你了,这边文章我要珍藏!我喜欢JS

    回复该留言

  3. 哎。。。逻辑好混乱,有点像绕口令。。。。。表示自己好菜

    回复该留言

  4. http://www.cn-cuckoo.com/main/wp-content/uploads/2009/12/named-function-expressions-demystified.html
    推荐博主看一下这篇

    回复该留言

    @xieranmaya, 啊,文末有这篇文章啊~我也看的是英文版的呢

2 Trackbacks For This Post

  1. [...] 对函数直接量的理解。这个需要注意IE和非IE浏览器的区别。详细信息可以猛击JavaScript的各种函数定义。 [...]

  2. [...] 与上例有一点儿不同的是,具名属性“exampleFunction”在全局执行上下文的变量实例化阶段创建,但是其并不指向任何函数对象,因为此时还为创建函数对象(译者注:通常这是由于JS解析引擎将var声明的变量语句提前执行,导致了变量声明和赋值是两个阶段完成)。直到代码执行到真正的函数表达式赋值语句时,才会创建这个函数对象,并让具名属性“exampleFunction”指向它。尽管函数对象创建的时间“较晚”,但其创建依然实在全局之下上下文中完成,因此其[[scope]]属性依然指向当前全局执行上下文,其中只包含一个全局对象(译者注:这一关键点是与上例相同的,因此他们的作用结果就是[[scope]]属性实际指向相同,因此也就能解释两种函数声明方式的类似,其实当了解了这些原理之后再来看JavaScript的各种函数定义,获取会有更深地理解)。 [...]

Leave a Reply

:wink: :twisted: :surprised: :smile: :sad: :rolleyes: :redface: :razz: :question: :neutral: :mrgreen: :mad: :lol: :idea: :exclaim: :evil: :eek: :cry: :cool: :confused: :biggrin: :arrow: