为啥使用命名函数表达式?

Posted

技术标签:

【中文标题】为啥使用命名函数表达式?【英文标题】:Why use named function expressions?为什么使用命名函数表达式? 【发布时间】:2013-02-26 11:56:18 【问题描述】:

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

命名函数表达式 (NFE)

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

匿名函数表达式

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

它们都可以用boo(); 调用。我真的不明白为什么/何时应该使用匿名函数以及何时应该使用命名函数表达式。它们之间有什么区别?

【问题讨论】:

blog.niftysnippets.org/2010/03/anonymouses-anonymous.html 相关:Why JavaScript function declaration (and expression)?,更普遍的是:var functionName = function() vs function functionName() 。 【参考方案1】:

在匿名函数表达式的情况下,函数是 anonymous - 从字面上看,它没有名称。您分配给它的变量有一个名称,但函数没有。 (更新:在 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

这是因为在添加新功能的过程中出现了信息泄露问题;我对另一个问题here的回答中的详细信息。

【讨论】:

值得注意的是,至少有两个地方使用 NFE 仍然具有具体的优势:首先,对于旨在通过 new 运算符用作构造函数的函数(给所有这些函数命名会使.constructor 属性在调试过程中更有用,用于弄清楚某个对象到底是什么的实例),以及直接传递给函数而不首先分配给属性或变量的函数文字(例如setTimeout(function () /*do stuff*/);)。甚至 Chrome 也会将它们显示为 (anonymous function),除非您通过命名它们来帮助它。 @MarkAmery: “这仍然是真的吗?我...尝试 CTRL-F 来获取这些规则,但找不到它们” 哦,是的。 :-) 它散布在整个规范中,而不是在一个地方定义一组规则,只需搜索“setFunctionName”。我在上面添加了一小部分链接,但它目前显示在大约 29 个不同的地方。如果您的setTimeout 示例没有从为setTimeout 声明的正式参数中获取名称(如果有的话),我只会感到有点惊讶。 :-) 但是是的,如果您知道自己不会处理对它们进行哈希处理的旧浏览器,那么 NFE 绝对有用。【参考方案2】:

如果函数需要引用自身(例如递归调用),命名函数很有用。实际上,如果您将文字函数表达式作为参数直接传递给另一个函数,则该函数表达式不能在 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(),方法是为函数表达式命名或在函数必须调用自身的地方使用函数声明。

(强调我的)

【讨论】:

【参考方案3】:

您应该始终使用命名函数表达式,这就是为什么:

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

    匿名函数在调试时没有帮助,因为您看不到导致问题的函数的名称。

    当你不命名一个函数时,以后更难理解它在做什么。给它起个名字更容易理解。

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

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

例如,在这里,因为名称 bar 是在函数表达式中使用的,所以它不会在外部范围内声明。对于命名函数表达式,函数表达式的名称包含在它自己的范围内。

【讨论】:

【参考方案4】:

如果将函数指定为函数表达式,则可以为其命名。

它只会在函数内部可用(IE8-除外)。

var f = function sayHi(name) 
  alert( sayHi ); // Inside the function you can see the function code
;

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

此名称用于可靠的递归函数调用,即使它被写入另一个变量。

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

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

test();

注意:使用函数声明无法做到这一点。此“特殊”内部函数名称仅在函数表达式语法中指定。

【讨论】:

【参考方案5】:

使用命名函数表达式更好,当您希望能够引用相关函数而不必依赖已弃用的功能(例如 arguments.callee)。

【讨论】:

这更像是一个评论而不是一个答案。也许详细说明会是有益的

以上是关于为啥使用命名函数表达式?的主要内容,如果未能解决你的问题,请参考以下文章

Scala中调用函数或者方法为啥有时候需要括号,有时候不需要括号

使用命名函数表达式编写递归函数 factorial

命名函数表达式探秘

为啥 `obj.foo = function() ;` 没有将名称 `foo` 分配给函数?

深入理解JavaScript系列:揭秘命名函数表达式

为啥指向函数的“const”指针不能在常量表达式中使用?