jscriptbug

来源:互联网 发布:湘南学院网络教学平台 编辑:程序博客网 时间:2024/06/07 18:12

JScript的bug

令人讨厌的是,JScript(也就是IE的ECMAScript实现)严重混淆了命名函数表达式。JScript搞得现如今很多人都站出来反对命名函数表达式。而且,直到JScript的最近一版——IE8中使用的5.8版——仍然存在下列的所有怪异问题。

下面我们就来看看IE在它的这个“破”实现中到底都搞出了哪些花样。唉,只有知已知彼,才能百战不殆嘛。请注意,为了清晰起见,我会通过一个个相对独立的小例子来说明这些问题,虽然这些问题很可能是一个主bug引起的一连串的后果。

例1:函数表达式的标识符渗透到外部(enclosing)作用域中

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

还有人记得吗,我们说过:命名函数表达式的标识符在其外部作用域中是无效的? 好啦,JScript明目张胆地违反了这一规定——上面例子中的标识符g被解析为函数对象。这是最让人头疼的一个问题了。这样,任何标识符都可能会在不经意间“污染”某个外部作用域——甚至是全局作用域。而且,这种污染常常就是那些难以捕获的bug的来源。

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

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

如前所述,在特定的执行环境中,函数声明会先于任何表达式被解析。上面这个例子展示了JScript实际上是把命名函数表达式当作函数声明了;因为它在“实际的”声明之前就解析了g

这个例子进而引出了下一个例子:

例3:命名函数表达式会创建两个截然不同的函数对象!

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

问题至此就比较严重了。或者可以说修改其中一个对象对另一个丝毫没有影响——这简直就是胡闹!通过例子可以看出,出现两个不同的对象会存在什么风险。假如你想利用缓存机制,在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函数对象(即返回2的那个函数对象。——译者注)。

聪明的读者可能会联想到:在将不同的函数对象与arguments.callee进行比较时,这个问题会有所表现吗?callee到底是引用f还是引用g呢?下面我们就来看一看:

  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的bug在这里又会出点乱子。由于JScript把命名函数表达式当作函数声明来解析(参见前面的“例2”),因此在变量声明阶段,f会被声明为局部变量。然后,在函数执行时,赋值语句已经不是未声明的了(因为f已经被声明为局部变量了。——译者注),右手边的function f(){}就会被直接赋给刚刚创建的局部变量f。而全局作用域中的f根本不会存在。

看完这个例子后,相信大家就会明白,如果你对JScript的“怪异”行为缺乏了解,你的代码中出现“严重不符合预期”的行为就不难理解了。

明白了JScript的缺陷以后,要采取哪些预防措施就非常清楚了。首先,要注意防范标识符泄漏(渗透)(不让标识符污染外部作用域)。其次,应该永远不引用被用作函数名称的标识符;还记得前面例子中那个讨人厌的标识符g吗?——如果我们能够当g不存在,可以避免多少不必要的麻烦哪。因此,关键就在于始终要通过f或者arguments.callee来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要把NFE(Named Funciont Expresssions,命名函数表达式)声明期间错误创建的函数清理干净

嗯,对于上面最后一点,我觉得还要再啰嗦两句:

JScript的内存管理

熟悉上述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,这样它就不会再引用多余的函数了    g = null;    return f;  })();

请注意,这里也明确声明了变量g,因此赋值语句g = null就不会在符合标准的客户端(如非JScript实现)中创建全局变量g了。通过废掉g的引用,垃圾收集器就可以把g引用的那个隐式创建的函数对象清除了。

在解决JScript NFE内存泄漏问题的过程中,我运行了一系列简单的测试,以便确定废掉g能够释放内存。

测试

这里的测试很简单。就是通过命名函数表达式创建10000个函数,把它们保存在一个数组中。过一会儿,看看这些函数到底占用了多少内存。然后,再废掉这些引用并重复这一过程。下面是我使用的一个测试用例:

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

通过运行在Windows XP SP2中的Process Explorer可以看到如下结果:

  IE6:    without `null`:   7.6K -> 20.3K    with `null`:      7.6K -> 18K  IE7:    without `null`:   14K -> 29.7K    with `null`:      14K -> 27K

这个结果大致验证了我的想法——显式地清除多余的引用确实可以释放内存,但释放的内存空间相对不多。在创建10000个函数对象的情况下,大约有3MB左右。对于大型应用程序,以及需要长时间运行或者在低内存设备(如手持设备)上运行的程序而言,这是绝对需要考虑的。但对小型脚本而言,这点差别可能也算不了什么。

有读者可能认为本文到此差不多就该结尾了——实际上还差得远呢 :)。我还想再多谈一点,这些内容涉及的是Safari 2.x。


原创网址:http://www.cn-cuckoo.com/main/wp-content/uploads/2009/12/named-function-expressions-demystified.html#named-expr

0 0