JScript的bug
令人讨厌的是,JScript(也就是IE的ECMAScript实现)严重混淆了命名函数表达式。JScript搞得现如今很多人都站出来反对命名函数表达式。而且,直到JScript的最近一版——IE8中使用的5.8版——仍然存在下列的所有怪异问题。
下面我们就来看看IE在它的这个“破”实现中到底都搞出了哪些花样。唉,只有知已知彼,才能百战不殆嘛。请注意,为了清晰起见,我会通过一个个相对独立的小例子来说明这些问题,虽然这些问题很可能是一个主bug引起的一连串的后果。
例1:函数表达式的标识符渗透到外部(enclosing)作用域中
cript > var f = function g(){};
typeof g; // "function"
还有人记得吗,我们说过:命名函数表达式的标识符在其外部作用域中是无效的? 好啦,JScript明目张胆地违反了这一规定——上面例子中的标识符DE>gDE>被解析为函数对象。这是最让人头疼的一个问题了。这样,任何标识符都可能会在不经意间“污染”某个外部作用域——甚至是全局作用域。而且,这种污染常常就是那些难以捕获的bug的来源。
例2:将命名函数表达式同时当作函数声明和函数表达式
cript > typeof g; // "function"
var f = function g(){};
如前所述,在特定的执行环境中,函数声明会先于任何表达式被解析。上面这个例子展示了JScript实际上是把命名函数表达式当作函数声明了;因为它在“实际的”声明之前就解析了DE>gDE>。
这个例子进而引出了下一个例子:
例3:命名函数表达式会创建两个截然不同的函数对象!
cript > var f = function g(){};
f === g; // false
f.expando = 'foo';
g.expando; // undefined
问题至此就比较严重了。或者可以说修改其中一个对象对另一个丝毫没有影响——这简直就是胡闹!通过例子可以看出,出现两个不同的对象会存在什么风险。假如你想利用缓存机制,在DE>fDE>的属性中保存某个信息,然后又想当然地认为可以通过引用相同对象的DE>gDE>的同名属性取得该信息,那么你的麻烦可就大了。
再来看一个稍微复杂点的情况。
例4:只管顺序地解析函数声明而忽略条件语句块
cript > var f = function g() {
return 1;
};
if (false) {
f = function g(){
return 2;
};
}
g(); // 2
要查找这个例子中的bug就要困难一些了。但导致bug的原因却非常简单。首先,DE>gDE>被当作函数声明解析,而由于JScript中的函数声明不受条件代码块约束(与条件代码块无关),所以在“该死的”DE>ifDE>分支中,DE>gDE>被当作另一个函数——DE>function g(){ return 2 }DE>——又被声明了一次。然后,所有“常规的”表达式被求值,而此时DE>fDE>被赋予了另一个新创建的对象的引用。由于在对表达式求值的时候,永远不会进入“该死的”DE>ifDE>分支,因此DE>fDE>就会继续引用第一个函数——DE>function g(){ return 1 }DE>。分析到这里,问题就很清楚了:假如你不够细心,在DE>fDE>中调用了DE>gDE>(在执行递归操作的时候会这样做。——译者注),那么实际上将会调用一个毫不相干的DE>gDE>函数对象(即返回2的那个函数对象。——译者注)。
聪明的读者可能会联想到:在将不同的函数对象与DE>arguments.calleeDE>进行比较时,这个问题会有所表现吗?DE>calleeDE>到底是引用DE>fDE>还是引用DE>gDE>呢?下面我们就来看一看:
cript > var f = function g(){
return [
arguments.callee == f,
arguments.callee == g
];
};
f(); // [true, false]
g(); // [false, true]
看到了吧,DE>arguments.calleeDE>引用的始终是被调用的函数。实际上,这应该是件好事儿,原因你一会儿就知道了。
另一个“意外行为”的好玩的例子,当我们在不包含声明的赋值语句中使用命名函数表达式时可以看到。不过,此时函数的名字必须与引用它的标识符相同才行:
cript > (function(){
f = function f(){};
})();
众所周知(但愿如此。——译者注),不包含声明的赋值语句(注意,我们不建议使用,这里只是出于示范需要才用的)在这里会创建一个全局属性DE>fDE>。而这也是标准实现的行为。可是,JScript的bug在这里又会出点乱子。由于JScript把命名函数表达式当作函数声明来解析(参见前面的“例2”),因此在变量声明阶段,DE>fDE>会被声明为局部变量。然后,在函数执行时,赋值语句已经不是未声明的了(因为f已经被声明为局部变量了。——译者注),右手边的DE>function f(){}DE>就会被直接赋给刚刚创建的局部变量DE>fDE>。而全局作用域中的DE>fDE>根本不会存在。
看完这个例子后,相信大家就会明白,如果你对JScript的“怪异”行为缺乏了解,你的代码中出现“严重不符合预期”的行为就不难理解了。
明白了JScript的缺陷以后,要采取哪些预防措施就非常清楚了。首先,要注意防范标识符泄漏(渗透)(不让标识符污染外部作用域)。其次,应该永远不引用被用作函数名称的标识符;还记得前面例子中那个讨人厌的标识符DE>gDE>吗?——如果我们能够当DE>gDE>不存在,可以避免多少不必要的麻烦哪。因此,关键就在于始终要通过DE>fDE>或者DE>arguments.calleeDE>来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要把NFE(Named Funciont Expresssions,命名函数表达式)声明期间错误创建的函数清理干净。
嗯,对于上面最后一点,我觉得还要再啰嗦两句:
JScript的内存管理
熟悉上述JScript缺陷之后,再使用这些有毛病的结构,就会发现内存占用方面的潜在问题。下面看一个简单的例子:
cript > var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();
我们知道,这里匿名(函数)调用返回的函数——带有标识符DE>gDE>的函数——被赋值给了外部的DE>fDE>。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。由于有一个多余的DE>gDE>函数被“截留”在了返回函数的闭包中,因此内存问题就出现了。这是因为(if语句)内部(的)函数与讨厌的DE>gDE>是在同一个作用域中被声明的。在这种情况下 ,除非我们显式地断开对(匿名调用返回的)DE>gDE>函数的引用,否则那个讨厌的家伙会一直占着内存不放。
cript > var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
// 废掉g,这样它就不会再引用多余的函数了
g = null;
return f;
})();
请注意,这里也明确声明了变量DE>gDE>,因此赋值语句DE>g = nullDE>就不会在符合标准的客户端(如非JScript实现)中创建全局变量DE>gDE>了。通过DE>废掉DE>对DE>gDE>的引用,垃圾收集器就可以把DE>gDE>引用的那个隐式创建的函数对象清除了。
在解决JScript NFE内存泄漏问题的过程中,我运行了一系列简单的测试,以便确定DE>废掉DE>DE>gDE>能够释放内存。
测试
这里的测试很简单。就是通过命名函数表达式创建10000个函数,把它们保存在一个数组中。过一会儿,看看这些函数到底占用了多少内存。然后,再废掉这些引用并重复这一过程。下面是我使用的一个测试用例:
cript > 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可以看到如下结果:
cript > 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。
Safari中存在的bug
在Safari较早的版本——Safari 2.x系列中,也存在一些鲜为人知的与NFE有关的bug。我在Web上看到有人说Safari 2.x不支持NFE 。实际上不是那么回事。Safari确实支持NFE,只不过它的实现中存在bug而已(很快你就会看到)。
在某些情况下,Safari 2.x遇到函数表达式时会出现不能完全解析程序的问题。而且,此时的Safari不会抛出任何错误(例如DE>SyntaxErrorDE>),只会“默默地知难而退”:
cript > (function f(){})(); // <== NFE
alert(1); // 由于前面的表达式破坏了整个程序,因此这一行永远不会执行
经过多次测试,我得出一个结论:Safari 2.x 不能解析非赋值表达式中的命名函数表达式。下面是一些赋值表达式的例子:
cript > // 变量声明
var f = 1;
// 简单赋值
f = 2, g = 3;
// 返回语句
(function(){
return (f = 2);
})();
换句话说,把命名函数表达式放到一个赋值表达式中会让Safari“很高兴”:
cript > (function f(){}); // 失败
var f = function f(){}; // 没问题
(function(){
return function f(){}; // 失败
})();
(function(){
return (f = function f(){}); // 没问题
})();
setTimeout(function f(){ }, 100); // 失败
Person.prototype = {
say: function say() { ... } // 失败
}
Person.prototype.say = function say(){ ... }; // 没问题
同时这也就意味着,在不使用赋值表达式的情况下,我们不能使用习以为常的模式返回命名函数表达式:
cript >
// 以下返回命名函数表达式的常见模式,对Safari 2.x来说是不兼容的:
(function(){
if (featureTest) {
return function f(){};
}
return function f(){};
})();
// 在Safari 2.x中,应该使用以下稍麻烦一点的方式:
(function(){
var f;
if (featureTest) {
f = function f(){};
}
else {
f = function f(){};
}
return f;
})();
// 或者,像下面这样也行:
(function(){
var f;
if (featureTest) {
return (f = function f(){});
}
return (f = function f(){});
})();
/*
可是,这样一来,就额外使用了一个对函数的引用,而该引用还被封闭在了返回函数的闭包中。
为了最大限度地降低额外的内存占用,可以考虑把所有命名函数表达式都赋值给一个变量。
*/
var __temp;
(function(){
if (featureTest) {
return (__temp = function f(){});
}
return (__temp = function f(){});
})();
...
(function(){
if (featureTest2) {
return (__temp = function g(){});
}
return (__temp = function g(){});
})();
/*
这样,后续的赋值语句通过“重用”前面的引用,达到了不过多占用内存的目的。
*/
如果兼容Safari 2.x非常重要,就应该保证源代码中不能出现任何“不兼容”的结构。虽然这样做不免会让人着急上火,可只要抓住了问题的根源,还是绝对能够做到的。
对了,还有个小问题必须说明一下:在Safari 2.x中声明命名函数时,函数的字符串表示不会包含函数的标识符:
cript > var f = function g(){};
// 看到了吗,函数的字符串表示中没有标识符g
String(f); // function () { }
这不算什么大问题。但正如我以前说过的,函数的反编译结果是无论如何也不能相信的 。
转自:http://lizenglang1.blog.163.com/blog/static/13626970120100802825211/
练习:
f = function() { return true; };
g = function() { return false; };
( function() {
if (g() && [] == ![]) {
f = function f() { return false; };
function g() { return true; };
}
})();
f(); // What's the result?
答题要求:
- 至少考虑 Chrome, Firefox, Safari, Opera, IE 五大浏览器。
- IE 考虑 IE6 到 IE9 beta, 其它浏览器考虑最新稳定版。
- 不能仅给一个结果,要说明为什么。