跟我学习JScript的Bug与内存管理
作者:小平果118 发布时间:2024-11-19 08:45:04
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的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。


猜你喜欢
- 前言本博客默认读者对神经网络与Tensorflow有一定了解,对其中的一些术语不再做具体解释。并且本博客主要以图片数据为例进行介绍,如有错误
- int(整型)在32位机器上,整数的位数为32位,取值范围为-2**31~2**31-1,即-2147483648~2147483647在6
- 今天群上面同志们在讨论css3内发光效果,自己也就研究一下,写了个效果出来,涉及css3投影,渐变,蒙版,伪类等知识点,现在写下设计思路,仅
- 我们的网络协议一般是把数据转换成JSON之后再传输。之前在Java里面,实现序列化和反序列化,不管是 jackson ,还是 fastjso
- 1.Js天数相加获取新日期function timestampToTime(timestamp) {  
- 新建项目如下图,比如sigma目录是我要上传的项目,在six-sigma目录下新建三个文件,分别是LICENSE也就是开源协议,README
- pandas 中 inplace 参数在很多函数中都会有,它的作用是:是否在原对象基础上进行修改inplace = True:不创建新的对象
- 本文实例为大家分享了python使用webdriver爬取微信公众号的具体代码,供大家参考,具体内容如下# -*- coding: utf-
- 前言需要导入以下包,没有的通过pip安装import matplotlib.pyplot as pltimport cv2from PIL
- 语法JavaScript的语法和Java语言类似,每个语句以;结束,语句块用{...}。但是,JavaScript并不强制要求在每个语句的结
- 前言今天,我无聊的时候做了一个搜索文章的软件,有没有更加的方便快捷不知道,好玩就行了。基于Python tkinter 制作文章
- 1.1 准备工作安装tableau安装MySQL数据库1.2 驱动包下载tableau连接MySQL数据库需要下载mysql对应的版本ODB
- 用v-model绑定单选框能带来很多便捷的开发体验。基础用法<template> <div id="app&qu
- 需求:从一台Oracle数据库获取数据,本以为是很简单的事情,直接将原来的SqlClient换成OracleClient调用,结果远没自己想
- 业务难点设计一个抽奖系统,这个系统并不是具体化,是抽象化,具有以下的几个难点:1、抽奖业务需要 复杂多变2、奖品类型和概率设置3、公平的抽奖
- 最近在公司的项目开发中使用到了 laravel 框架,采用的是前后端开发的模式。接触过前后端开发模式的小伙伴应该都知道,后端返回的数据格式需
- 最近需要将实验数据画图出来,由于使用python进行实验,自然使用到了matplotlib来作图。下面的代码可以作为画图的模板代码,代码中有
- Python中的五种特性:切片,迭代,列表生成式,生成器,迭代器。切片切片就相当于其他语言中的截断函数,取部分指定元素用的。L = list
- 这个url的正则表达式判断的js!是比较全面的。它验证的情况包括!IP,域名(domain),ftp,二级域名,域名中的文件,域名加上端口!
- 我们通常说的双机热备是指两台机器都在运行,但并不是两台机器都同时在提供服务。当提供服务的一台出现故障的时候,另外一台会马上自动接管并且提供服