深刻理解JavaScript系列(2):揭秘命名函数表达式

前言

网上还没用发现有人对命名函数表达式进去重复深刻的讨论,正由于如此,网上出现了各类各样的误解,本文将从原理和实践两个方面来探讨JavaScript关于命名函数表达式的优缺点。javascript

简单的说,命名函数表达式只有一个用户,那就是在Debug或者Profiler分析的时候来描述函数的名称,也可使用函数名实现递归,但很快你就会发现实际上是不切实际的。固然,若是你不关注调试,那就没什么可担忧的了,不然,若是你想了解兼容性方面的东西的话,你仍是应该继续往下看看。html

咱们先开始看看,什么叫函数表达式,而后再说一下现代调试器如何处理这些表达式,若是你已经对这方面很熟悉的话,请直接跳过此小节。前端

函数表达式和函数声明

在ECMAScript中,建立函数的最经常使用的两个方法是函数表达式和函数声明,二者期间的区别是有点晕,由于ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是你们常说的函数名称),而函数表达式则能够省略这个标示符:java

  函数声明:web

  function 函数名称 (参数:可选){ 函数体 }json

  函数表达式:数组

  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;
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开发中友个经常使用的模式是基于对某种特性的测试来假装函数定义,从而达到性能优化的目的,但因为这种方式都是在同一做用域内,因此基本上必定要用函数表达式:

  // 该代码来自Garrett Smith的APE Javascript library库(http://dhtmlkitchen.com/ape/) 
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"

既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名?

正如咱们开头所说:给它一个名字就是可让调试过程更方便,由于在调试的时候,若是在调用栈中的每一个项都有本身的名字来描述,那么调试过程就太爽了,感觉不同嘛。

调试器中的函数名

若是一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(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()

OK,又学了一招吧?不过在高兴以前,咱们再看看不一样寻常的JScript吧。

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来引用函数。若是你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,必定要把命名函数表达式声明期间错误建立的函数清理干净

对于,上面最后一点,咱们还得再解释一下。

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

如咱们所料,显示断开引用能够释放内存,可是释放的内存不是不少,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是很是有必要的。

 

关于在Safari 2.x中JS的解析也有一些bug,但介于版本比较低,因此咱们在这里就不介绍了,你们若是想看的话,请仔细查看英文资料。

SpiderMonkey的怪癖

你们都知道,命名函数表达式的标识符只在函数的局部做用域中有效。但包含这个标识符的局部做用域又是什么样子的吗?其实很是简单。在命名函数表达式被求值时,会建立一个特殊的对象,该对象的惟一目的就是保存一个属性,而这个属性的名字对应着函数标识符,属性的值对应着那个函数。这个对象会被注入到当前做用域链的前端。而后,被“扩展”的做用域链又被用于初始化函数。

在这里,有一点十分有意思,那就是ECMA-262定义这个(保存函数标识符的)“特殊”对象的方式。标准说“像调用new Object()表达式那样”建立这个对象。若是从字面上来理解这句话,那么这个对象就应该是全局Object的一个实例。然而,只有一个实现是按照标准字面上的要求这么作的,这个实现就是SpiderMonkey。所以,在SpiderMonkey中,扩展Object.prototype有可能会干扰函数的局部做用域:

  Object.prototype.x = 'outer';

(function(){

var x = 'inner';

/*
函数foo的做用域链中有一个特殊的对象——用于保存函数的标识符。这个特殊的对象实际上就是{ foo: <function object> }。
当经过做用域链解析x时,首先解析的是foo的局部环境。若是没有找到x,则继续搜索做用域链中的下一个对象。下一个对象
就是保存函数标识符的那个对象——{ foo: <function object> },因为该对象继承自Object.prototype,因此在此能够找到x。
而这个x的值也就是Object.prototype.x的值(outer)。结果,外部函数的做用域(包含x = 'inner'的做用域)就不会被解析了。
*/

(function foo(){

alert(x); // 提示框中显示:outer

})();
})();

不过,更高版本的SpiderMonkey改变了上述行为,缘由多是认为那是一个安全漏洞。也就是说,“特殊”对象再也不继承Object.prototype了。不过,若是你使用Firefox 3或者更低版本,还能够“重温”这种行为。

另外一个把内部对象实现为全局Object对象的是黑莓(Blackberry)浏览器。目前,它的活动对象(Activation Object)仍然继承Object.prototype。但是,ECMA-262并无说活动对象也要“像调用new Object()表达式那样”来建立(或者说像建立保存NFE标识符的对象同样建立)。 人家规范只说了活动对象是规范中的一种机制。 

那咱们就来看看黑莓里都发生了什么:

  Object.prototype.x = 'outer';

(function(){

var x = 'inner';

(function(){

/*
在沿着做用域链解析x的过程当中,首先会搜索局部函数的活动对象。固然,在该对象中找不到x。
但是,因为活动对象继承自Object.prototype,所以搜索x的下一个目标就是Object.prototype;而
Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差很少,
包含x = 'inner'的外部函数的做用域(活动对象)就不会被解析了。
*/

alert(x); // 显示:outer

})();
})();

不过神奇的仍是,函数中的变量甚至会与已有的Object.prototype的成员发生冲突,来看看下面的代码:

  (function(){

var constructor = function(){ return 1; };

(function(){

constructor(); // 求值结果是{}(即至关于调用了Object.prototype.constructor())而不是1

constructor === Object.prototype.constructor; // true
toString === Object.prototype.toString; // true

// ……

})();
})();

要避免这个问题,要避免使用Object.prototype里的属性名称,如toString, valueOf, hasOwnProperty等等。

 

JScript解决方案

  var fn = (function(){

// 声明要引用函数的变量
var f;

// 有条件地建立命名函数
// 并将其引用赋值给f
if (true) {
f = function F(){ }
}
else if (false) {
f = function F(){ }
}
else {
f = function F(){ }
}

// 声明一个与函数名(标识符)对应的变量,并赋值为null
// 这其实是给相应标识符引用的函数对象做了一个标记,
// 以便垃圾回收器知道能够回收它了
var F = null;

// 返回根据条件定义的函数
return f;
})();

最后咱们给出一个应用上述技术的应用实例,这是一个跨浏览器的addEvent函数代码:

  // 1) 使用独立的做用域包含声明
var addEvent = (function(){

var docEl = document.documentElement;

// 2) 声明要引用函数的变量
var fn;

if (docEl.addEventListener) {

// 3) 有意给函数一个描述性的标识符
fn = function addEvent(element, eventName, callback) {
element.addEventListener(eventName, callback, false);
}
}
else if (docEl.attachEvent) {
fn = function addEvent(element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
}
}
else {
fn = function addEvent(element, eventName, callback) {
element['on' + eventName] = callback;
}
}

// 4) 清除由JScript建立的addEvent函数
// 必定要保证在赋值前使用var关键字
// 除非函数顶部已经声明了addEvent
var addEvent = null;

// 5) 最后返回由fn引用的函数
return fn;
})();

替代方案

其实,若是咱们不想要这个描述性名字的话,咱们就能够用最简单的形式来作,也就是在函数内部声明一个函数(而不是函数表达式),而后返回该函数:

  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用户的很容易就会被相应实现的 细节搞得晕头转向。

要解决这个问题,固然就得想一套更合理的命名方案了。但关键是不要再额外制造麻烦。我如今能想起来的方案大概有以下几个:

  'addEvent', 'altAddEvent', 'fallbackAddEvent'
// 或者
'addEvent', 'addEvent2', 'addEvent3'
// 或者
'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'

另外,这种模式还存在一个小问题,即增长内存占用。提早建立N个不一样名字的函数,等于有N-1的函数是用不到的。具体来说,若是document.documentElement 中包含attachEvent,那么addEventListener 和addEventAsProperty则根本就用不着了。但是,他们都占着内存哪;并且,这些内存将永远都得不到释放,缘由跟JScript臭哄哄的命名表达式相同——这两个函数都被“截留”在返回的那个函数的闭包中了。

不过,增长内存占用这个问题确实没什么大不了的。若是某个库——例如Prototype.js——采用了这种模式,无非也就是多建立一两百个函数而已。只要不是(在运行时)重复地建立这些函数,而是只(在加载时)建立一次,那么就没有什么好担忧的。

WebKit的displayName

WebKit团队在这个问题采起了有点儿另类的策略。介于匿名和命名函数如此之差的表现力,WebKit引入了一个“特殊的”displayName属性(本质上是一个字符串),若是开发人员为函数的这个属性赋值,则该属性的值将在调试器或性能分析器中被显示在函数“名称”的位置上。Francisco Tolmasky详细地解释了这个策略的原理和实现

 

将来考虑

未来的ECMAScript-262第5版(目前仍是草案)会引入所谓的严格模式(strict mode)。开启严格模式的实现会禁用语言中的那些不稳定、不可靠和不安全的特性。听说出于安全方面的考虑,arguments.callee属性将在严格模式下被“封杀”。所以,在处于严格模式时,访问arguments.callee会致使TypeError(参见ECMA-262第5版的10.6节)。而我之因此在此提到严格模式,是由于若是在基于第5版标准的实现中没法使用arguments.callee来执行递归操做,那么使用命名函数表达式的可能性就会大大增长。从这个意义上来讲,理解命名函数表达式的语义及其bug也就显得更加剧要了。

  // 此前,你可能会使用arguments.callee
(function(x) {
if (x <= 1) return 1;
return x * arguments.callee(x - 1);
})(10);

// 但在严格模式下,有可能就要使用命名函数表达式
(function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x - 1);
})(10);

// 要么就退一步,使用没有那么灵活的函数声明
function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x - 1);
}
factorial(10);

致谢

理查德· 康福德(Richard Cornford),是他率先解释了JScript中命名函数表达式所存在的bug。理查德解释了我在这篇文章中说起的大多数bug,因此我强烈建议你们去看看他的解释。我还要感谢Yann-Erwan Perio道格拉斯·克劳克佛德(Douglas Crockford),他们早在2003年就在comp.lang.javascript论坛中说起并讨论NFE问题了

约翰-戴维·道尔顿(John-David Dalton)对“最终解决方案”提出了很好的建议。

托比·兰吉的点子被我用在了“替代方案”中。

盖瑞特·史密斯(Garrett Smith)德米特里·苏斯尼科(Dmitry Soshnikov)对本文的多方面做出了补充和修正。

相关文章
相关标签/搜索