zoukankan      html  css  js  c++  java
  • <深入理解JavaScript>学习笔记(2)_揭秘命名函数表达式

    写在前面的话

    注:本文是拜读了 深入理解JavaScript 之后深有感悟,故做次笔记方便之后查看。

    感觉这章的内容有点深奥....略难懂啊。

    先坐下笔记,加深一下印象吧。

    我主要记一下自己感觉有用的东西...哈哈

    函数表达式和函数声明

    在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语句构成的)。 

    JScript的Bug

    比较恶的是,IE的ECMAScript实现JScript严重混淆了命名函数表达式,搞得现很多人都出来反对命名函数表达式,而且即便是最新的一版(IE8中使用的5.8版)仍然存在下列问题。

    下面我们就来看看IE在实现中究竟犯了那些错误,俗话说知已知彼,才能百战不殆。我们来看看如下几个例子:

    例1:函数表达式的标示符泄露到外部作用域

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

    上面我们说过,命名函数表达式的标示符在外部作用域是无效的,但JScript明显是违反了这一规范,上面例子中的标示符g被解析成函数对象,这就乱了套了,很多难以发现的bug都是因为这个原因导致的。

    注:IE9貌似已经修复了这个问题

    例2:将命名函数表达式同时当作函数声明和函数表达式

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

    特性环境下,函数声明会优先于任何表达式被解析,上面的例子展示的是JScript实际上是把命名函数表达式当成函数声明了,因为它在实际声明之前就解析了g。

    这个例子引出了下一个例子。
    例3:命名函数表达式会创建两个截然不同的函数对象!

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

    看到这里,大家会觉得问题严重了,因为修改任何一个对象,另外一个没有什么改变,这太恶了。通过这个例子可以发现,创建2个不同的对象,也就是说如果你想修改f的属性中保存某个信息,然后想当然地通过引用相同对象的g的同名属性来使用,那问题就大了,因为根本就不可能。

    再来看一个稍微复杂的例子:

    例4:仅仅顺序解析函数声明而忽略条件语句块

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

    这个bug查找就难多了,但导致bug的原因却非常简单。首先,g被当作函数声明解析,由于JScript中的函数声明不受条件代码块约束,所以在这个很恶的if分支中,g被当作另一个函数function g(){ return 2 },也就是又被声明了一次。然后,所有“常规的”表达式被求值,而此时f被赋予了另一个新创建的对象的引用。由于在对表达式求值的时候,永远不会进入“这个可恶if分支,因此f就会继续引用第一个函数function g(){ return 1 }。分析到这里,问题就很清楚了:假如你不够细心,在f中调用了g,那么将会调用一个毫不相干的g函数对象。

    你可能会文,将不同的对象和arguments.callee相比较时,有什么样的区别呢?我们来看看:

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

    可以看到,arguments.callee的引用一直是被调用的函数,实际上这也是好事,稍后会解释。

    还有一个有趣的例子,那就是在不包含声明的赋值语句中使用命名函数表达式:

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

    按照代码的分析,我们原本是想创建一个全局属性f(注意不要和一般的匿名函数混淆了,里面用的是带名字的生命),JScript在这里捣乱了一把,首先他把表达式当成函数声明解析了,所以左边的f被声明为局部变量了(和一般的匿名函数里的声明一样),然后在函数执行的时候,f已经是定义过的了,右边的function f(){}则直接就赋值给局部变量f了,所以f根本就不是全局属性。

    了解了JScript这么变态以后,我们就要及时预防这些问题了,首先防范标识符泄漏带外部作用域,其次,应该永远不引用被用作函数名称的标识符;还记得前面例子中那个讨人厌的标识符g吗?——如果我们能够当g不存在,可以避免多少不必要的麻烦哪。因此,关键就在于始终要通过f或者arguments.callee来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要把命名函数表达式声明期间错误创建的函数清理干净

    对于,上面最后一点,我们还得再解释一下。

    这章的东西确实有点偏,我们只简单了解下就可以。其他的个人感觉用处不大,不做笔记了。

  • 相关阅读:
    【常用】source insight常用设置及快捷键
    【Linux学习】配置环境:实现【VirtualBox + ubuntu】+【开启ssh服务】+【putty远程连接到虚拟机】
    javascript:区别浏览器
    linux 之centos6.3 安装中文输入法
    前端优化(静态资源)
    javascript 学习心得!
    编程总结
    2019年春季学期第二周作业
    HTTP could not register URL http://+:8000/testservice/. Your process does not have access rights to this namespace 解决方案
    [原]ASP.NET MVC 3 Razor + jqGrid 示例
  • 原文地址:https://www.cnblogs.com/178mz/p/4530501.html
Copyright © 2011-2022 走看看