深入理解JavaScript系列 ----(2):揭秘命名函数表达式

揭秘命名函数表达式

函数表达式和函数声明:

函数声明一定要有函数名,而函数表达式不一定需要:

函数声明:

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

函数表达式:

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


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

  // 千万别这样做!
  // 因为有的浏览器会返回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();

为保持各个浏览器的正确性,推荐使用函数表达式.


IE的JScrip一些bug:

例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根本就不是全局属性。


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

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

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


JScript中的内存管理:

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

我们知道,这个匿名函数调用返回的函数(带有标识符g的函数),然后赋值给了外部的f。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。所以这个多余的g函数就死在了返回函数的闭包中了,因此内存问题就出现了。这是因为if语句内部的函数与g是在同一个作用域中被声明的。这种情况下 ,除非我们显式断开对g函数的引用,否则它一直占着内存不放。

  var f = (function(){
    var f, g;
    if (true) {
      f = function g(){};
    }
    else {
      f = function g(){};
    }
    // 设置g为null以后它就不会再占内存了
    g = null;
    return f;
  })();

通过设置g为null,垃圾回收器就把g引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。

  Object.prototype.x = 'outer';
  
  (function(){
    
    var x = 'inner';
    
    (function(){
      
      /*
      在沿着作用域链解析x的过程中,首先会搜索局部函数的活动对象。当然,在该对象中找不到x。
      可是,由于活动对象继承自Object.prototype,因此搜索x的下一个目标就是Object.prototype;而
      Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差不多,
      包含x = 'inner'的外部函数的作用域(活动对象)就不会被解析了。
      */
      
      alert(x); // 显示:outer
      
    })();
  })();

不过神奇的还是,函数中的变量甚至会与已有的Object.prototype的成员发生冲突,来看看下面的代码:

  (function(){
    
    var constructor = function(){ return 1; };
    
    (function(){
      
      constructor(); // 求值结果是{}(即相当于调用了Object.prototype.constructor())而不是1
      
      constructor === Object.prototype.constructor; // true
      toString === Object.prototype.toString; // true
      
      // ……
      
    })();
  })();

要避免这个问题,要避免使用Object.prototype里的属性名称,如toString, valueOf, hasOwnProperty等等。


  // 1) 使用独立的作用域包含声明
  var addEvent = (function(){

    var docEl = document.documentElement;

    // 2) 声明要引用函数的变量
    var fn;

    if (docEl.addEventListener) {

      // 3) 有意给函数一个描述性的标识符
      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函数
    //    一定要保证在赋值前使用var关键字
    //    除非函数顶部已经声明了addEvent
    var addEvent = null;

    // 5) 最后返回由fn引用的函数
    return fn;
  })();

  // 此前,你可能会使用arguments.callee
  (function(x) {
    if (x <= 1) return 1;
    return x * arguments.callee(x - 1);
  })(10);
  
  // 但在严格模式下,有可能就要使用命名函数表达式
  (function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
  })(10);
  
  // 要么就退一步,使用没有那么灵活的函数声明
  function factorial(x) {
    if (x <= 1) return 1;
    return x * factorial(x - 1);
  }
  factorial(10);


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值