跟我学习 JScript 的 Bug 与内存管理,小编对 JScript 的 Bug 与内存管理也不甚了解,所以整理了本篇文章,希望可以解决大家学习时的困扰。
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
1、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 来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要把命名函数表达式声明期间错误创建的函数清理干净。
2、JScript 的内存管理
知道了这些不符合规范的代码解析 bug 以后,我们如果用它的话,就会发现内存方面其实是有问题的,来看一个例子:
- 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 引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。
测试
测试很简单,就是命名函数表达式创建 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 中的任务管理器可以看到如下结果:
- IE7:
- without `null`: 7.6K -> 20.3K
- with `null`: 7.6K -> 18K
- IE8:
- without `null`: 14K -> 29.7K
- with `null`: 14K -> 27K
如我们所料,显示断开引用可以释放内存,但是释放的内存不是很多,10000 个函数对象才释放大约 3M 的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。
来源: