在Javascript中,什么时候需要将命名函数赋值给变量?

11

当我在 Babel JS 的在线 REPL(http://babeljs.io/repl/)中输入以下内容时:

let a = (x) => x+1

它将被转译成:

"use strict";

var a = function a(x) {
  return x + 1;
};

这里的var a = function a(x)让我有点困惑,因为按照我的理解,要么用var a = function(x),要么用function a(x)就足够了。

是否有人知道在什么情况下以及为什么需要将一个命名函数分配给一个变量?


2
这并非必须,但如果函数没有名称,则在某些浏览器的堆栈跟踪中不会显示为“function a”。因此,命名函数对于简化调试可能是有益的。否则它们将显示为“匿名函数”。 - Shilly
@Alnitak: 错了。只有在函数定义而不是函数表达式时,它才会将其名称暴露给范围(并进行提升)。这是一个(命名)函数表达式,并且只能通过变量(在本例中具有相同的名称)访问。但请考虑变量称为 x 的情况,您将使用 x() 调用函数 a,因为 a() 未定义基本上,独立行上的函数定义 function lol(){} 将与 var lol = function lol(){} 相同。 - Zorgatone
我只是觉得你在谈论示例中的命名函数(这是错误的),而不是一般的命名函数。我只是想为OP和读者指出,并非所有命名函数(即表达式与声明)都会被提升(然后在作用域中可用)。祝好,兄弟 =] - Zorgatone
@Zorgatone 说实话,另一个答案存在更多问题;-) - Alnitak
2
我觉得评论和答案中有太多噪音。因此,这里是tl;dr:1)箭头函数是表达式,而不是声明,因此我们需要使用函数表达式。2)在ES6中,函数名称有时从分配给它的变量中推断出来,因此使用命名函数表达式。理想情况下,可以只分配给a.name ='a',但并非所有浏览器都支持这一点。 - Felix Kling
显示剩余5条评论
3个回答

8

这里实际上有两个不同的问题:

  1. 定义或表达函数的不同方式之间有什么区别?
  2. 为什么let a = (x) => x + 1会被转译成这样?

为了回答问题(2),我们需要理解问题(1)-- 这已经在SO和其他地方广泛讨论过。


问题1

让我们来看一下你提到的三种选择:

函数声明:

function a(x) { ... }

从语法上讲,这些函数必须始终以function参考)开头。它们在解析时被提升,并在本地作用域中创建一个命名函数。 (匿名)函数表达式:
var a = function (x) { ... }

var a本身将在解析时进行提升,但在运行时直到执行此行之前它都是undefined

命名函数表达式:

var a = function a(x) { ... }

尽管语法看起来像是对一个函数声明进行赋值,但实际上这只是一个带有名称的函数表达式。我觉得这很令人困惑,但这就是语法。
函数声明和函数表达式之间的主要区别在于,使用声明,您可以执行以下操作:
a(1);
function a(x) { return x + 1; }

尝试使用函数表达式(命名或匿名)实现这一点会导致错误。

问题2

  1. 为什么let a = (x) => x + 1会被转换成这样?

我们将箭头函数 (x) => x + 1 赋值给一个块级作用域变量 let,因此我们应该期望在运行时执行此行之后,才会定义 a。这应该是一个函数表达式,而不是函数声明

最后,为什么 let a = (x) => x + 1 被转换为命名函数表达式而不是匿名函数表达式?有什么区别吗?正如Alnitak和其他人指出的:

  • 函数名称出现在调试器中,可以帮助调试。
  • 命名函数定义内部的作用域具有对函数本身的引用。这允许递归和访问包含函数的属性。
所以所谓的“命名函数表达式”具有某些匿名函数表达式没有的优点。但实际上对于这里应该发生什么似乎存在分歧。根据MDN
箭头函数始终是匿名的
this answerWhy use named function expressions?的回答则表示:
“[自ES6起]许多‘匿名’函数表达式创建具有名称的函数,这是由各种现代JavaScript引擎在从上下文推断名称方面非常聪明之前的先例... 这散布在规范中。”
其他参考资料:

我发现最好的方法是通过玩转 Babel REPL 来掌握这个。


1
我看不出你所做的区别 - 你提议的 Babel 输出唯一的区别是函数表达式中缺少函数名称,在 外部 作用域中没有任何区别 - 它只在函数作用域内有所不同。 - Alnitak
1
啊,我明白你的意思了——你使用了“if”这个词让它感到困惑,因为你描述的(几乎)就是Babel实际上所做的事情。更相关的是,如果Babel使用函数定义而不是函数表达式,那么你引用的代码就不会产生错误,但实际上它应该会产生错误。 - Alnitak
1
我认为这样做并不是为了“安全” - 将ES2015 lambda转换为函数表达式只是正确的做法。 - Alnitak
1
哦,而提升发生在“解析”时间,而不是执行时间。 - Alnitak
我认为将代码转译成 var a = function a() {} 也会导致错误,请参考这个 JsFiddle(http://jsfiddle.net/9abuc116/)。 - Hanfei Sun
显示剩余7条评论

4

如果你写下:

function a(x) { }

当函数被提升到封闭作用域的顶部时,a将立即在整个作用域中的解析时间可用。

然而,如果你写成:

var a = function a(x) { }

如果这行代码没有被执行,var a 将不会在封闭的作用域中有一个 定义值。但是,在该函数内部,一个 不同a 会存在于作为函数本身的本地作用域引用中。

通过使用 let a = function ... 结构,Babel 与后者形式更加一致,确保 a 在运行时分配给一个(命名的)函数表达式,而不是使用解析时间的 函数声明


那么 var a = function(x){} 呢? - Hanfei Sun
这与我的第二个例子完全相同,只是函数现在没有对自身的内部引用 - 只有在外部分配给它的a - Alnitak
调试器不会在调用堆栈中显示函数名称,因为该函数是匿名的。 - Zorgatone

2

看起来这是根据标准 (12.14.4):

AssignmentExpression[In, Yield] : LeftHandSideExpression[?Yield] = AssignmentExpression[?In, ?Yield]

1. 如果 LeftHandSideExpression 不是 ObjectLiteral 或 ArrayLiteral,则
  a. 让 lref 成为评估 LeftHandSideExpression 的结果。
  b. 如果出现异常,就返回它。
  c. 让 rref 成为评估 AssignmentExpression 的结果。
  d. 让 rval 成为 GetValue(rref) 的结果。
  e. 如果 AssignmentExpression 是匿名函数定义并且 LeftHandSideExpression 的 IsIdentifierRef 也为 true,则
    I.   让 hasNameProperty 成为 HasOwnProperty(rval, "name") 的结果。
    II.  如果出现异常,就返回它。
    III. 如果 hasNameProperty 为 false,则执行 SetFunctionName(rval, GetReferencedName(lref))

因此,每当对一个未命名的函数表达式进行分配并赋值给一个命名的标识符时,函数名称应该被设置为标识符名称。
Babel 遵循这个过程,并生成兼容的 ES5 实现。Chrome(v46.0.2490.71,V8 引擎...)不遵循这个过程,在这种情况下,`name` 等于 `''`。
关于这个问题本身...
在Javascript中,何时需要将命名函数分配给变量?
答案是从不。使用命名函数取决于开发人员是否需要使用名称(例如在“字符串化”函数时),或者调试需求(更好的堆栈跟踪...)。

我不同意“从不”的说法。递归呢? - a better oliver
@zeroflagL - 无法在评论中发布代码,但是... jsfiddle - Amit
1
你的例子实际上是错误的,因为 iefe 的返回值(无论是什么值)不算作“匿名函数定义”。Babel 对此进行了正确而完整的实现。 - Bergi
@Bergi - 很有趣。我误解了IsAnonymousFunctionDefinition(AssignmentExpression),但你是对的。AssignmentExpression解析,但不被评估。谢谢! - Amit
那个例子当然可以工作。但是如果我将函数分配给另一个变量并为a分配一个新值,那么它就会出错,如果该函数没有名称来调用自身。 - a better oliver
@zeroflagL - 是的,那是真的。虽然有点牵强,但确实如此 :-)。然而,这并不改变我对“从不”的看法。即使是为了递归,没有必要将命名函数分配给变量 - Amit

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