深入理解JavaScript系列(2):揭秘命名函数表达式
来源:互联网 发布:parsley.js 中文提示 编辑:程序博客网 时间:2024/05/01 16:59
前言
函数表达式和函数声明
在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是有点晕,因为ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符:
函数声明:
function 函数名称 (参数:可选){ 函数体 }
函数表达式:
function 函数名称(可选)(参数:可选){ 函数体 }
如何判断是函数声明还是函数表达式呢?ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。function foo(){} // 声明,因为它是程序的一部分 var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分 new function bar(){}; // 表达式,因为它是new表达式 (function(){ function bar(){} // 声明,因为它是函数体的一部分 })();
还有一种函数表达式不太常见,就是被括号括住的(function foo(){}),他是表达式的原因是因为括号 ()是一个分组操作符,它的内部只能包含表达式,我们来看几个例子:
function foo(){} // 函数声明 (function foo(){}); // 函数表达式:包含在分组操作符内 try { (var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句 } catch(err) { // SyntaxError }
表达式和声明存在着十分微妙的差别,首先,函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值,
alert(fn()); function fn() { return 'Hello world!'; }
函数语句
命名函数表达式
提到命名函数表达式,理所当然,就是它得有名字,前面的例子var bar = function foo(){};就是一个有效的命名函数表达式,但有一点需要记住:这个名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效:
var f = function foo(){ return typeof foo; // foo是在内部作用域内有效 }; // foo在外部用于是不可见的 typeof foo; // "undefined" f(); // "function"
调试器中的函数名
JScript的Bug
例1:函数表达式的标示符泄露到外部作用域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
看到这里,大家会觉得问题严重了,因为修改任何一个对象,另外一个没有什么改变,这太恶了。通过这个例子可以发现,创建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的引用一直是被调用的函数
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中的任务管理器可以看到如下结果:
IE6: without `null`: 7.6K -> 20.3K with `null`: 7.6K -> 18K IE7: without `null`: 14K -> 29.7K with `null`: 14K -> 27K
替代方案
其实,如果我们不想要这个描述性名字的话,我们就可以用最简单的形式来做,也就是在函数内部声明一个函数(而不是函数表达式),然后返回该函数:var hasClassName = (function(){ // 定义私有变量 var cache = { }; // 使用函数声明 function hasClassName(element, className) { var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)'; var re = cache[_className] || (cache[_className] = new RegExp(_className)); return re.test(element.className); } // 返回函数 return hasClassName; })();
显然,当存在多个分支函数定义时,这个方案就不行了。不过有种模式貌似可以实现:那就是提前使用函数声明来定义所有函数,并分别为这些函数指定不同的标识符:
var addEvent = (function(){ var docEl = document.documentElement; function addEventListener(){ /* ... */ } function attachEvent(){ /* ... */ } function addEventAsProperty(){ /* ... */ } if (typeof docEl.addEventListener != 'undefined') { return addEventListener; } elseif (typeof docEl.attachEvent != 'undefined') { return attachEvent; } return addEventAsProperty; })();
虽然这个方案很优雅,但也不是没有缺点。第一,由于使用不同的标识符,导致丧失了命名的一致性。且不说这样好还是坏,最起码它不够清晰。有人喜欢使用相同的名字,但也有人根本不在乎字眼上的差别。可毕竟,不同的名字会让人联想到所用的不同实现。例如,在调试器中看到attachEvent,我们就知 道
addEvent
是基于attachEvent
的实现。当 然,基于实现来命名的方式也不一定都行得通。假如我们要提供一个API,并按照这种方式把函数命名为inner。那么API用户的很容易就会被相应实现的 细节搞得晕头转向。另外,这种模式还存在一个小问题,即增加内存占用。提前创建N个不同名字的函数,等于有N-1的函数是用不到的。具体来讲,如果document.documentElement
中包含attachEvent
,那么addEventListener
和addEventAsProperty
则根本就用不着了。可是,他们都占着内存哪;而且,这些内存将永远都得不到释放,原因跟JScript臭哄哄的命名表达式相同——这两个函数都被“截留”在返回的那个函数的闭包中了。
不过,增加内存占用这个问题确实没什么大不了的。如某个库——例如Prototype.js——采用了这种模式,无非也就是多创建一两百个函数而已。只要不是(在运行时)重复地创建这些函数,而是只(在加载时)创建一次,那么就没有什么好担心的。
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列 ----(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- (转)深入理解JavaScript系列(2):揭秘命名函数表达式
- 深入理解JavaScript系列————揭秘命名函数表达式
- 深入JavaScript(2)揭秘命名函数表达式
- 揭秘命名函数表达式
- (转)深入理解JavaScript系列(4):立即调用的函数表达式
- 深入理解JavaScript系列(4):立即调用的函数表达式
- 深入理解JavaScript系列(4):立即调用的函数表达式
- Linux系统NFS服务器的配置方法
- jsp:由rs.last()方法不可用,学习ResultSet游标笔记 .
- OpenNI 2 對 Kinect 的支援
- android通过wifi进行粗略定位(google maps)操作办法
- 1009. Product of Polynomials
- 深入理解JavaScript系列(2):揭秘命名函数表达式
- ABAP "FOR ALL ENTRIES IN" 使用指南
- struts 向后台传值
- 使用C#选择文件夹、打开文件夹、选择文件或者如何使用C#选择文件夹
- Linux下配置安装NFS
- Matrix学习2、Matrix的基本三种变换之Scale
- 【VC++编译 常见错误】Warning、Error、Fatal Error、Link
- 如何在Linux中查看进程占用内存情况
- 下载linux kernel 代码压缩包