简单的说,命名函数表达式只有一个用户,那就是在Debug或者Profiler分析的时候来描述函数的名称,也可使用函数名实现递归,但很快你就会发现实际上是不切实际的。固然,若是你不关注调试,那就没什么可担忧的了,不然,若是你想了解兼容性方面的东西的话,你仍是应该继续往下看看。html
咱们先开始看看,什么叫函数表达式,而后再说一下现代调试器如何处理这些表达式。web
在ECMAScript中,建立函数的最经常使用的两个方法是函数表达式和函数声明,二者期间的区别是有点晕,由于ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是你们常说的函数名称),而函数表达式则能够省略这个标示符:json
函数声明:数组
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 }
你能够会想到,在使用eval对JSON进行执行的时候,JSON字符串一般被包含在一个圆括号里:eval('(' + json + ')'),这样作的缘由就是由于分组操做符,也就是这对括号,会让解析器强制将JSON的花括号解析成表达式而不是代码块。学习
try { { "x": 5 }; // "{" 和 "}" 作解析成代码块 } catch(err) { // SyntaxError } ({ "x": 5 }); // 分组操做符强制将"{" 和 "}"做为对象字面量来解析
表达式和声明存在着十分微妙的差异,首先,函数声明会在任何表达式被解析和求值以前先被解析和求值,即便你的声明在代码的最后一行,它也会在同做用域内第一个表达式以前被解析/求值,参考以下例子,函数fn是在alert以后声明的,可是在alert执行的时候,fn已经有定义了:
alert(fn()); function fn() { return 'Hello world!'; }
另外,还有一点须要提醒一下,函数声明在条件语句内虽然能够用,可是没有被标准化,也就是说不一样的环境可能有不一样的执行结果,因此这样状况下,最好使用函数表达式:
// 千万别这样作! // 由于有的浏览器会返回first的这个function,而有的浏览器返回的倒是第二个 if (true) { function foo() { return 'first'; } } else { function foo() { return 'second'; } } foo(); // 相反,这样状况,咱们要用函数表达式 var foo; if (true) { foo = function() { return 'first'; }; } else { foo = function() { return 'second'; }; } foo();
函数声明的实际规则以下:
注:函数声明只能出如今程序或函数体内。从句法上讲,它们 不能出如今Block(块)({ ... })中,例如不能出如今 if、while 或 for 语句中。由于 Block(块) 中只能包含Statement语句, 而不能包含函数声明这样的源元素。另外一方面,仔细看一看规则也会发现,惟一可能让表达式出如今Block(块)中情形,就是让它做为表达式语句的一部分。可是,规范明确规定了表达式语句不能以关键字function开头。而这实际上就是说,函数表达式一样也不能出如今Statement语句或Block(块)中(由于Block(块)就是由Statement语句构成的)。
在ECMAScript的语法扩展中,有一个是函数语句,目前只有基于Gecko的浏览器实现了该扩展,因此对于下面的例子,咱们仅是抱着学习的目的来看,通常来讲不推荐使用(除非你针对Gecko浏览器进行开发)。
1.通常语句能用的地方,函数语句也能用,固然也包括Block块中:
if (true) { function f(){ } } else { function f(){ } }
2.函数语句能够像其余语句同样被解析,包含基于条件执行的情形
if (true) { function foo(){ return 1; } } else { function foo(){ return 2; } } foo(); // 1 // 注:其它客户端会将foo解析成函数声明 // 所以,第二个foo会覆盖第一个,结果返回2,而不是1
3.函数语句不是在变量初始化期间声明的,而是在运行时声明的——与函数表达式同样。不过,函数语句的标识符一旦声明能在函数的整个做用域生效了。标识符有效性正是致使函数语句与函数表达式不一样的关键所在(下一小节咱们将会展现命名函数表达式的具体行为)。
// 此刻,foo还没用声明 typeof foo; // "undefined" if (true) { // 进入这里之后,foo就被声明在整个做用域内了 function foo(){ return 1; } } else { // 历来不会走到这里,因此这里的foo也不会被声明 function foo(){ return 2; } } typeof foo; // "function"
不过,咱们可使用下面这样的符合标准的代码来模式上面例子中的函数语句:
var foo;
// 'undefined' if (true) { foo = function foo(){ return 1; }; } else { foo = function foo() { return 2; }; }
4.函数语句和函数声明(或命名函数表达式)的字符串表示相似,也包括标识符:
if (true) { function foo(){ return 1; } } String(foo); // function foo() { return 1; }
5.另一个,早期基于Gecko的实现(Firefox 3及之前版本)中存在一个bug,即函数语句覆盖函数声明的方式不正确。在这些早期的实现中,函数语句不知何故不能覆盖函数声明:
// 函数声明 function foo(){ return 1; } if (true) { // 用函数语句重写 function foo(){ return 2; } } foo(); // FF3如下返回1,FF3.5以上返回2 // 不过,若是前面是函数表达式,则没用问题 var foo = function(){ return 1; }; if (true) { function foo(){ return 2; } } foo(); // 全部版本都返回2
再次强调一点,上面这些例子只是在某些浏览器支持,因此推荐你们不要使用这些,除非你就在特性的浏览器上作开发.
函数表达式在实际应用中仍是很常见的,在web开发中友个经常使用的模式是基于对某种特性的测试来假装函数定义,从而达到性能优化的目的,但因为这种方式都是在同一做用域内,因此基本上必定要用函数表达式:
var contains = (function() { var docEl = document.documentElement; if (typeof docEl.compareDocumentPosition != 'undefined') { return function(el, b) { return (el.compareDocumentPosition(b) & 16) !== 0; }; } else if (typeof docEl.contains != 'undefined') { return function(el, b) { return el !== b && el.contains(b); }; } return function(el, b) { if (el === b) return false; while (el != b && (b = b.parentNode) != null); return el === b; }; })();
提到命名函数表达式,理所固然,就是它得有名字,前面的例子var bar = function foo(){};就是一个有效的命名函数表达式,但有一点须要记住:这个名字只在新定义的函数做用域内有效,由于规范规定了标示符不能在外围的做用域内有效:
var f = function foo(){ return typeof foo; // foo是在内部做用域内有效 }; // foo在外部用因而不可见的 typeof foo; // "undefined" f(); // "function"
既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名?
正如咱们开头所说:给它一个名字就是可让调试过程更方便,由于在调试的时候,若是在调用栈中的每一个项都有本身的名字来描述,那么调试过程就太爽了,感觉不同嘛。
var f = function foo(){ return typeof foo; // foo是在内部做用域内有效 }; // foo在外部用因而不可见的 typeof foo; // "undefined" f(); // "function"
既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名?
正如咱们开头所说:给它一个名字就是可让调试过程更方便,由于在调试的时候,若是在调用栈中的每一个项都有本身的名字来描述,那么调试过程就太爽了,感觉不同嘛。
若是一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(Firebug)有时候还会为大家函数取名并显示,让他们和那些应用该函数的便利具备相同的角色,但是一般状况下,这些调试器只安装简单的规则来取名,因此说没有太大价格,咱们来看一个例子
function foo(){ return bar(); } function bar(){ return baz(); } function baz(){ debugger; } foo(); // 这里咱们使用了3个带名字的函数声明 // 因此当调试器走到debugger语句的时候,Firebug的调用栈上看起来很是清晰明了 // 由于很明白地显示了名称 baz bar foo expr_test.html()
经过查看调用栈的信息,咱们能够很明了地知道foo调用了bar, bar又调用了baz(而foo自己有在expr_test.html文档的全局做用域内被调用),不过,还有一个比较爽地方,就是刚才说的Firebug为匿名表达式取名的功能:
function foo(){ return bar(); } var bar = function(){ return baz(); } function baz(){ debugger; } foo(); // Call stack baz bar() //看到了么? foo expr_test.html()
而后,当函数表达式稍微复杂一些的时候,调试器就不那么聪明了,咱们只能在调用栈中看到问号:
function foo(){ return bar(); } var bar = (function(){ if (window.addEventListener) { return function(){ return baz(); }; } else if (window.attachEvent) { return function() { return baz(); }; } })(); function baz(){ debugger; } foo(); // Call stack baz (?)() // 这里但是问号哦 foo expr_test.html()
另外,当把函数赋值给多个变量的时候,也会出现使人郁闷的问题:
function foo(){ return baz(); } var bar = function(){ debugger; }; var baz = bar; bar = function() { alert('spoofed'); }; foo(); // Call stack: bar() foo expr_test.html()
这时候,调用栈显示的是foo调用了bar,但实际上并不是如此,之因此有这种问题,是由于baz和另一个包含alert('spoofed')的函数作了引用交换所致使的。
归根结底,只有给函数表达式取个名字,才是最委托的办法,也就是使用命名函数表达式。咱们来使用带名字的表达式来重写上面的例子(注意当即调用的表达式块里返回的2个函数的名字都是bar):
function foo(){ return bar(); } var bar = (function(){ if (window.addEventListener) { return function bar(){ return baz(); }; } else if (window.attachEvent) { return function bar() { return baz(); }; } })(); function baz(){ debugger; } foo(); // 又再次看到了清晰的调用栈信息了耶! baz bar foo expr_test.html()
比较恶的是,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
再来看一个稍微复杂的例子:
例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来引用函数。若是你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,必定要把命名函数表达式声明期间错误建立的函数清理干净。
对于,上面最后一点,咱们还得再解释一下。
知道了这些不符合规范的代码解析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
如咱们所料,显示断开引用能够释放内存,可是释放的内存不是不少,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是很是有必要的。
关于在Safari 2.x中JS的解析也有一些bug,但介于版本比较低,因此咱们在这里就不介绍了,你们若是想看的话,请仔细查看英文资料。