为什么要使用命名函数表达式?

102

在JavaScript中,我们有两种不同的方式来进行函数表达式:

命名函数表达式(NFE)

var boo = function boo () {
  alert(1);
};

匿名函数表达式:

var boo = function () {
  alert(1);
};

这两者都可以使用boo();调用。我真的看不出为什么/何时应该使用匿名函数和何时应该使用具名函数表达式。它们之间有什么区别?


1
http://blog.niftysnippets.org/2010/03/anonymouses-anonymous.html - Bergi
5个回答

96
在匿名函数表达式中,函数是匿名的——字面上来说,它没有名称。你分配给它的变量有一个名称,但函数本身没有。 (更新:这在ES5之前是正确的。从ES2015 [又称为ES6]开始,使用匿名表达式创建的函数通常会得到一个真正的名称[但不是自动标识符],请继续阅读…)。
名称是有用的。名称可以在堆栈跟踪、调用堆栈、断点列表等中看到。名称是一件好事™。
(在旧版本的IE [IE8及以下版本]中,您曾经需要注意命名函数表达式,因为它们错误地在两个完全不同的时间创建了两个完全不同的函数对象[更多信息请参见我的博客文章Double take]。如果您需要支持IE8 [!!],最好使用匿名函数表达式或函数声明,但要避免命名函数表达式。)
命名函数表达式的一个关键点是,在函数体内为该函数创建一个在范围内的标识符。

var x = function example() {
    console.log(typeof example); // "function"
};
x();
console.log(typeof example);     // "undefined"

截至ES2015,许多“匿名”函数表达式创建带有名称的函数,这是由各种现代JavaScript引擎在上下文中推断名称而先于此的。在ES2015中,您的匿名函数表达式将生成一个名为boo的函数。但是,即使使用ES2015+语义,也不会创建自动标识符。

var obj = {
    x: function() {
       console.log(typeof x);   // "undefined"
       console.log(obj.x.name); // "x"
    },
    y: function y() {
       console.log(typeof y);   // "function"
       console.log(obj.y.name); // "y"
    }
};
obj.x();
obj.y();

函数名称的分配是通过规范中各种操作中使用的SetFunctionName抽象操作完成的。
简而言之,当匿名函数表达式出现在类似于赋值或初始化的右侧时,就会使用这个操作。
var boo = function() { /*...*/ };

(或者它可以是letconst而不是var,或者

var obj = {
    boo: function() { /*...*/ }
};

或者

doSomething({
    boo: function() { /*...*/ }
});

(最后两个其实是同一件事),得到的函数将有一个名称(例如示例中的boo)。

有一个重要且故意的例外情况:对现有对象上的属性进行赋值:

obj.boo = function() { /*...*/ }; // <== Does not get a name

这是由于在新功能添加过程中引起的信息泄露担忧,有关详细信息请参见我回答另一个问题 此处

1
值得注意的是,至少有两个地方使用NFE仍然具有明显的优势:首先,对于旨在通过new运算符用作构造函数的函数(为所有这些函数命名使.constructor属性在调试期间更有用,以确定某个对象是实例的原因),其次是直接传递给函数的函数文字,而不是首先分配给属性或变量(例如setTimeout(function () {/*do stuff*/});)。即使是Chrome也将它们显示为“(匿名函数)”,除非您通过命名来帮助它。 - Mark Amery
4
这段话的意思是:@MarkAmery问:“这仍然正确吗?我试图使用CTRL-F查找这些规则,但找不到它们。” 是的。 :-) 它们分散在规范中,而不是在一个定义一组规则的地方,只需搜索“setFunctionName”。我已经添加了上面的一小部分链接,但它目前出现在大约29个不同的位置。如果setTimeout有形式参数声明,那么我可能只会感到稍微惊讶您的示例没有捕获名称。:-) 但是,如果您知道不会涉及将其搞砸的旧浏览器,则NFE肯定非常有用。 - T.J. Crowder

26

命名函数有用处,如果它们需要引用自身(例如递归调用)。确实,如果您将文字函数表达式直接作为参数传递给另一个函数,则在 ES5 严格模式下,该函数表达式 无法 直接引用自身,除非它被命名。

例如,考虑以下代码:

setTimeout(function sayMoo() {
    alert('MOO');
    setTimeout(sayMoo, 1000);
}, 1000);
如果传递给 setTimeout 的函数表达式是匿名的,那么要以这种干净的方式编写此代码几乎是不可能的。我们需要在调用 setTimeout 之前将其分配给一个变量。使用命名函数表达式的方式则更加简短和整洁。
历史上甚至可以使用匿名函数表达式编写此类代码,通过利用 arguments.callee ...
setTimeout(function () {
    alert('MOO');
    setTimeout(arguments.callee, 1000);
}, 1000);

... 但是 arguments.callee 已经被弃用,并且在 ES5 严格模式下完全禁止使用。因此,MDN 建议:

通过为函数表达式命名或者在必须调用自身的地方使用函数声明来避免使用 arguments.callee()

(我强调)


4

你应该始终使用命名函数表达式,原因如下:

  1. 当你需要递归时,可以使用该函数的名称。

  2. 匿名函数在调试时无法帮助你查看导致问题的函数名称。

  3. 如果不给函数命名,后来很难理解它在做什么。给它一个名称会使它更容易理解。

var foo = function bar() {
    //some code...
};

foo();
bar(); // Error!

例如,在这里,因为名称“bar”是在函数表达式内使用的,所以它不会在外部范围中声明。对于命名的函数表达式,函数表达式的名称被封装在其自己的作用域内。

3
如果一个函数被指定为函数表达式,可以给它命名。它只能在函数内部使用(除IE8及以下版本)。
var f = function sayHi(name) {
  alert( sayHi ); // Inside the function you can see the function code
};

alert( sayHi ); // (Error: undefined variable 'sayHi')

这个名字是为了可靠的递归函数调用而设计的,即使它被写到另一个变量中。

此外,NFE(命名函数表达式)的名称可以通过Object.defineProperty(...)方法进行覆盖,如下所示:

var test = function sayHi(name) {
  Object.defineProperty(test, 'name', { value: 'foo', configurable: true });
  alert( test.name ); // foo
};

test();

注意:使用函数声明无法实现此操作。这个“特殊”的内部函数名称仅在函数表达式语法中指定。

1

当您想要引用函数本身而不依赖于已弃用的特性,如arguments.callee时,使用命名函数表达式更好。


3
这更像是一条评论而不是回答。也许进行阐述会有益处。 - vsync

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接