zoukankan      html  css  js  c++  java
  • 你不知道的JavaScript--Item7 函数和(命名)函数表达式

    1、函数声明与函数表达式

    在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是有点晕,因为ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符:

    函数声明:

    function 函数名称 (参数:可选){ 函数体 }

    函数表达式:

    function 函数名称(可选)(参数:可选){ 函数体 }

    所以,可以看出,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,如何判断是函数声明还是函数表达式呢?ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。

    function foo(){} // 声明,因为它是程序的一部分
    var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
    
    new function bar(){}; // 表达式,因为它是new表达式
    
    (function(){
        function bar(){} // 声明,因为它是函数体的一部分
    })();

    表达式和声明存在着十分微妙的差别,首先,函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值,参考如下例子,函数fn是在alert之后声明的,但是在alert执行的时候,fn已经有定义了:

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

    另外,还有一点需要提醒一下,函数声明在条件语句内虽然可以用,但是没有被标准化,也就是说不同的环境可能有不同的执行结果,所以这样情况下,最好使用函数表达式: 因为在条件语句中没有块级作用域这个概念

    // 千万别这样做!
    // 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个
    
    if (true) {
      function foo() {
        return 'first';
      }
    }
    else {
      function foo() {
        return 'second';
      }
    }
    foo();
    
    // 相反,这样情况,我们要用函数表达式
    var foo;
    if (true) {
      foo = function() {
        return 'first';
      };
    }
    else {
      foo = function() {
        return 'second';
      };
    }
    foo();

    函数声明的实际规则如下:

    函数声明只能出现在程序或函数体内。从句法上讲,它们 不能出现在Block(块)({ … })中,例如不能出现在 if、while 或 for 语句中。因为 Block(块) 中只能包含Statement语句, 而不能包含函数声明这样的源元素。另一方面,仔细看一看规则也会发现,唯一可能让表达式出现在Block(块)中情形,就是让它作为表达式语句的一部分。但是,规范明确规定了表达式语句不能以关键字function开头。而这实际上就是说,函数表达式同样也不能出现在Statement语句或Block(块)中(因为Block(块)就是由Statement语句构成的)。

    2、命名函数表达式

    提到命名函数表达式,理所当然,就是它得有名字,前面的例子var bar = function foo(){};就是一个有效的命名函数表达式,但有一点需要记住:这个名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效:

    var f = function foo(){
      return typeof foo; // function --->foo是在内部作用域内有效
    };
    // foo在外部用于是不可见的
    typeof foo; // "undefined"
    f(); // "function"

    既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名?

    正如我们开头所说:给它一个名字就是可以让调试过程更方便,因为在调试的时候,如果在调用栈中的每个项都有自己的名字来描述,那么调试过程就太爽了,感受不一样嘛。

    tips:这里提出一个小问题:在ES3中,命名函数表达式的作用域对象也继承了 Object.prototype 的属性。这意味着仅仅是给函数表达式命名也会将 Object.prototype 中的所有属性引入到作用域中。结果可能会出人意料。

    var constructor = function(){return null;}
    var f = function f(){
        return construcor();
    }
    f(); //{in ES3 环境}

    该程序看起来会产生 null, 但其实会产生一个新的对象。因为命名函数表达式在其作用域内继承了 Object.prototype.constructor(即 Object 的构造函数)。就像 with 语句一样,这个作用域会因 Object.prototype 的动态改变而受到影响。幸运的是,ES5 修正了这个错误。

    这种行为的一个合理的解决办法是创建一个与函数表达式同名的局部变量并赋值为 null。即使在没有错误地提升函数表达式声明的环境中,使用 var 重声明变量能确保仍然会绑定变量 g。设置变量 g 为 null 能确保重复的函数可以被垃圾回收。

    var f = function g(){
        return 17;
    }
    var g =null;

    3、调试器(调用栈)中的命名函数表达式

    刚才说了,命名函数表达式的真正用处是调试,那到底怎么用呢?如果一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(Firebug)有时候还会为你们函数取名并显示,让他们和那些应用该函数的便利具有相同的角色,可是通常情况下,这些调试器只安装简单的规则来取名,所以说没有太大价值,我们来看一个例子:不用命名函数表达式

    function foo(){
      return bar();
    }
    function bar(){
      return baz();
    }
    function baz(){
      debugger;
    }
    foo();
    
    // 这里我们使用了3个带名字的函数声明
    // 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了 
    // 因为很明白地显示了名称
    baz
    bar
    foo
    expr_test.html()

    通过查看调用栈的信息,我们可以很明了地知道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()

    然后,当函数表达式稍微复杂一些的时候,调试器就不那么聪明了,我们只能在调用栈中看到问号:

    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
    (?)() // 这里可是问号哦,显示为匿名函数(anonymous function)
    foo
    expr_test.html()

    另外,当把函数赋值给多个变量的时候,也会出现令人郁闷的问题:

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

    这时候,调用栈显示的是foo调用了bar,但实际上并非如此,之所以有这种问题,是因为baz和另外一个包含alert(‘spoofed’)的函数做了引用交换所导致的。

    归根结底,只有给函数表达式取个名字,才是最委托的办法,也就是使用命名函数表达式。我们来使用带名字的表达式来重写上面的例子(注意立即调用的表达式块里返回的2个函数的名字都是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();
    
    // 又再次看到了清晰的调用栈信息了耶!
    baz
    bar
    foo
    expr_test.html()

    OK,又学了一招吧?


    系列文章导航:

    1、你不知道的JavaScript–Item1 严格模式

    2、你不知道的JavaScript–Item2 浮点数精度

    3、你不知道的JavaScript–Item3 隐式强制转换

    4、你不知道的JavaScript–Item4 基本类型和基本包装类型(引用类型)

    5、你不知道的JavaScript–Item5 全局变量

    6、你不知道的JavaScript–Item6 var预解析与函数声明提升(hoist )

    7、你不知道的JavaScript–Item7 函数和(命名)函数表达式

    8、你不知道的JavaScript–Item8 函数,方法,构造函数调用

    9、你不知道的JavaScript–Item9 call(),apply(),bind()与回调

    10、你不知道的JavaScript–Item10 闭包(closure)

    11、你不知道的JavaScript–Item11 arguments对象

    12、你不知道的JavaScript–Item12 undefined 与 null

    13、你不知道的JavaScript–Item13 理解 prototype, getPrototypeOf 和_ proto_

    14、你不知道的JavaScript–Item14 使用prototype的几点注意事项

    15、你不知道的JavaScript–Item15 prototype原型和原型链详解

    16、你不知道的JavaScript–Item16 for 循环和for…in 循环的那点事儿

    17、你不知道的JavaScript–Item17 循环与prototype最后的几点小tips

    18、你不知道的JavaScript–Item18 JScript的Bug与内存管理

    19、你不知道的JavaScript–Item19 执行上下文(execution context)

    20、你不知道的JavaScript–Item20 作用域与作用域链(scope chain)

    21、你不知道的JavaScript–Item21 漂移的this


    持续更新中……………….

    版权声明:本文为小平果原创文章,转载请注明:http://blog.csdn.net/i10630226

  • 相关阅读:
    Log4net实例(转自http://zjuoliver.blog.163.com/blog/static/5101920084299524443/)
    不同数据库获取新增加的主键值
    asp.net中的ALERT类
    Log4net操作指南(转自http://www.cnblogs.com/dragon/archive/2005/03/24/124254.html)
    阅读器关闭时尝试调用Read无效
    LINQ中文教程LINQ初体验之LINQ to Object
    vs2010设置默认浏览器
    附加数据库时出现错误解决办法
    oracle安装后,第一次登陆的步骤
    已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭
  • 原文地址:https://www.cnblogs.com/dingxiaoyue/p/4948219.html
Copyright © 2011-2022 走看看