为什么匿名函数表达式和命名函数表达式的初始化方式如此不同?

5
我正在查看第13节或ECMAScript规范(v.5)。匿名函数表达式的初始化如下:

根据FormalParameterListopt指定的参数和FunctionBody指定的主体,按照13.2中指定的方式创建一个新的Function对象。将运行执行上下文的词法环境作为作用域传入。如果FunctionExpression包含在严格代码中或其FunctionBody是严格代码,则将true作为Strict标志传入。

这个逻辑与命名函数表达式的初始化非常相似。但是,请注意命名函数表达式的初始化方式有多么不同。
  1. 将运行执行上下文的词法环境作为参数调用NewDeclarativeEnvironment方法,将结果赋值给funcEnv。
  2. 将envRec设为funcEnv的环境记录。
  3. 调用envRec的CreateImmutableBinding具体方法,将标识符的字符串值作为参数传递。
  4. 按照13.2中指定的规则创建一个新的函数对象,参数由FormalParameterListopt指定,函数体由FunctionBody指定。将funcEnv作为作用域传入。如果FunctionExpression包含在严格代码中或者它的FunctionBody是严格代码,则将Strict标志设置为true。
  5. 调用envRec的InitializeImmutableBinding具体方法,将标识符的字符串值和闭包作为参数传递。
  6. 返回闭包。

我知道命名/匿名函数表达式之间的一个重要区别是,命名函数表达式可以从函数内部进行递归调用,但这是我所能想到的全部。为什么设置如此不同,并且为什么需要执行这些额外的步骤?

2个回答

9
所有这些“跳舞”的原因很简单。在函数作用域内但不在外部使命名函数表达式的标识符可用。
typeof f; // undefined

(function f() {
  typeof f; // function
})();

如何在函数中使f可用?
由于f不能在外部词法环境中使用,因此无法创建绑定。同时,在内部变量环境中也无法创建绑定,因为此时还未创建它;函数在实例化时尚未执行,因此10.4.3(进入函数代码)步骤及其NewDeclarativeEnvironment从未发生。
因此,这是通过创建一个“继承”直接来自当前词法环境的中间词法环境,并将其作为[[Scope]]传递到新创建的函数中来完成的。
如果我们将步骤13拆分为伪代码,则可以清楚地看到这一点:
// create new binding layer
funcEnv = NewDeclarativeEnvironment(current Lexical Environment)

envRec = funcEnv
// give it function's identifier
envRec.CreateImmutableBinding(Identifier)

// create function with this intermediate binding layer
closure = CreateNewFunction(funcEnv)

// assign newly created function to an identifier within this intermediate binding layer
envRec.InitializeImmutableBinding(Identifier, closure)

因此,当解析标识符(例如)时,f 中的词法环境现在看起来像这样:

(function f(){

  [global environment] <- [f: function(){}] <- [Current Variable Environment]

})();

使用匿名函数的话,代码会变成这样:
(function() {

  [global environment] <- [Current Variable Environment]

})();

2
还有其他微妙之处。函数表达式名称绑定是只读的,但您仍然可以在函数表达式体内使用相同的名称声明变量或函数。描述这种语义(请记住这只是一个规范)需要使用额外的环境记录。 - Allen Wirfs-Brock
有趣。但是为什么需要额外的环境记录?例如,如果 NFE 的标识符绑定在函数声明(10.5)期间创建,而不是在第 5 步之前创建,那么源代码中的任何 var/function 声明都将覆盖 NFE 的绑定(5f),而不是遮蔽它。实际上产生了相同的效果,不是吗? - kangax

1
两者的核心区别在于作用域(尽管,如果没有其他问题,看到这么多内容涉及作用域还是很奇妙的 ;) - 而您已经正确地指出了命名/匿名函数表达式之间的主要区别在于调用命名函数递归时更加方便

为什么我说方便呢?好吧,实际上没有什么可以阻止您递归调用匿名函数,但这只是不太美观而已:

//silly factorial, 5!
(function(n) {
  if (n<=1) return 1;
  return (n*arguments.callee(n-1)); //arguments.callee is so 1990s!
})(5);

实际上,这正是MDN在描述命名函数表达式时所说的内容!

如果你想在函数体内引用当前函数,你需要创建一个命名函数表达式。这个名称仅在函数体(作用域)中本地使用。这也避免了使用非标准的arguments.callee属性。


1
而在严格模式下,arguments.callee是不被允许的。 - jfriend00
@jfriend00:我并不是在说它是:)即使在非严格模式下,它也已经被弃用/不建议使用了。就我所知,这正是命名函数表达式存在的原因——允许递归——我的意思是函数标识符作为命名仅局限于函数范围内! - Oleg
我只是添加了一条额外的信息,作为不使用arguments.callee的另一个理由。不需要有防御性的语气。 - jfriend00
2
我并不是在为自己辩护,我只是在保护自己!#公平 #有道理 - Oleg

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