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; // falsef.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.3Kwith `null`:      7.6K -> 18KIE7:without `null`:   14K -> 29.7Kwith `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

最新文章

  1. 姚班、智班之后,量子信息班在清华成立,姚期智担纲,“致力国家战略需求”!...
  2. 4 年开发 43 款软件,这位乡村教师火了
  3. XCode4.3.3 + iOS5.1 无证书开发并生成app、ipa文件
  4. unittest 多个测试文件只开一次浏览器_接口测试平台代码实现75: 多接口用例15
  5. android 通知历史,Android 4.3人性新功能:查看通知历史
  6. 自学python前戏
  7. 小c下载样式插件Xiaocstyle适用于emlog系统
  8. android activity焦点,android启动activity文本框不获得焦点
  9. 集成学习框架-学习小结20161121
  10. 解决 No module named ‘tensorflow.examples.tutorials‘
  11. linux怎么新建系统用户名,在 Linux 中不使用 useradd 命令如何创建用户账号
  12. 第2章 神经网络的数学基础
  13. 利用Lua脚本语言制作魔兽WOW插件
  14. gcc中使用rpath指定优先搜索路径
  15. r语言 求平方和_R语言 第3章 R语言常用的数据管理(10)
  16. javaScript重定向页面
  17. C++char数据类型
  18. backtrack渗透测试中常用的命令总结
  19. 8、content-scripts实现一个简单的去除广告插件
  20. Arduino ESP8266/ESP32读取和改写MAC

热门文章

  1. [转] 怎么固定KMPlayer窗口大小
  2. 「游戏」c++贪吃蛇1.1
  3. 历史二—— 浮点运算与数组下标寻址
  4. 有什么好玩的网页小游戏网站推荐么?
  5. 【Linux实验】用户和组群账户管理
  6. 税法18个税种全总结附记忆小口诀
  7. 微博宕机复盘:什么样的技术架构,可支持80个明星并发出轨?
  8. 如何将mov格式的视频转换mp4?
  9. access横向求和sum_access 2007使用查询对数据求和
  10. runtime error解决方法