关于javascript:为什么使用命名函数表达式?

Why use named function expressions?

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

命名函数表达式(NFE):

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

匿名函数表达式:

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

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


在匿名函数表达式的情况下,该函数是匿名的 —从字面上看,它没有名字。您分配给它的变量有一个名称,但该函数没有。 (更新:通过ES5确实如此。从ES2015 [aka ES6]开始,通常使用匿名表达式创建的函数获得真实名称[但不是自动标识符],请继续阅读...)

名称很有用。名称可以在堆栈跟踪,调用堆栈,断点列表等中看到。名称是一件好事吗?

(你曾经不得不提防旧版IE [IE8及以下]中的命名函数表达式,因为它们错误地在两个完全不同的时间创建了两个完全独立的函数对象[更多在我的博客文章Double take]。如果你需要支持IE8 [!!],最好坚持使用匿名函数表达式或函数声明,但要避免使用命名函数表达式。)

关于命名函数表达式的一个关键点是,它为functon体中的函数创建了一个具有该名称的范围内标识符:

1
2
3
4
5
var x = function example() {
    console.log(typeof example); //"function"
};
x();
console.log(typeof example);     //"undefined"

但是,从ES2015开始,许多"匿名"函数表达式创建了带有名称的函数,这是各种现代JavaScript引擎在从上下文推断名称时非常聪明的。在ES2015中,您的匿名函数表达式将生成名为boo的函数。但是,即使使用ES2015 +语义,也不会创建自动标识符:

1
2
3
4
5
6
7
8
9
10
11
12
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抽象操作完成的。

简短版本基本上是匿名函数表达式出现在赋值或初始化等右侧的任何时候,如:

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

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

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

要么

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

(最后两个实际上是相同的东西),结果函数将有一个名称(boo,在示例中)。

有一个重要的,有意的例外:分配给现有对象的属性:

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

这是因为当新功能正在进行添加过程时引发的信息泄漏问题;我在这里回答另一个问题的细节。


如果命名函数需要引用自身(例如,用于递归调用),则命名函数很有用。实际上,如果您将文字函数表达式作为参数直接传递给另一个函数,那么该函数表达式不能在ES5严格模式下直接引用自身,除非它被命名。

例如,请考虑以下代码:

1
2
3
4
setTimeout(function sayMoo() {
    alert('MOO');
    setTimeout(sayMoo, 1000);
}, 1000);

如果传递给setTimeout的函数表达式是匿名的,那么就不可能干净地编写这段代码。我们需要在setTimeout调用之前将其分配给变量。这样,使用命名函数表达式,稍微短一些。

通过利用arguments.callee,在历史上可以使用匿名函数表达式编写这样的代码...

1
2
3
4
setTimeout(function () {
    alert('MOO');
    setTimeout(arguments.callee, 1000);
}, 1000);

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

Avoid using arguments.callee() by either giving function expressions a name or use a function declaration where a function must call itself.

(强调我的)


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

它只能在函数内部使用(IE8-除外)。

1
2
3
4
5
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(...)方法覆盖,如下所示:

1
2
3
4
5
6
var test = function sayHi(name) {
  Object.defineProperty(test, 'name', { value: 'foo', configurable: true });
  alert( test.name ); // foo
};

test();

注意:使用"功能声明"时无法执行此操作。此"特殊"内部函数名称仅在函数表达式语法中指定。


你应该总是使用命名函数表达式,这就是为什么:

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

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

  • 当你没有命名一个函数时,以后就更难理解它正在做什么。 给它起一个名字会让它更容易理解。

  • 1
    2
    3
    4
    5
    6
    var foo = function bar() {
        //some code...
    };

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

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


    当您希望能够引用有问题的函数而不必依赖于已弃用的功能(如arguments.callee)时,使用命名函数表达式会更好。