解释封装的匿名函数语法

402

总结

你能解释一下JavaScript中封装的匿名函数的语法背后的原理吗?为什么这个能用:(function(){})();,但这个不行:function(){}();


我的了解

在JavaScript中,创建命名函数的方式如下:

function twoPlusTwo(){
    alert(2 + 2);
}
twoPlusTwo();

您也可以创建一个匿名函数并将其分配给变量:

var twoPlusTwo = function(){
    alert(2 + 2);
};
twoPlusTwo();

你可以通过创建匿名函数,将其用括号包裹并立即执行来封装一段代码:

(function(){
    alert(2 + 2);
})();

当创建模块化脚本时,使用这种方法可以避免向当前作用域或全局作用域中添加可能会产生冲突的变量,例如在Greasemonkey脚本、jQuery插件等情况下。

现在我明白了这种方法的原理。花括号将其内容封闭起来,并仅暴露结果(我确定有更好的描述方式),例如(2 + 2) === 4


我不明白的地方

但是我不明白为什么这种方式同样有效:

function(){
    alert(2 + 2);
}();
你能解释一下吗?

45
我认为,最初使用JavaScript时,各种符号和定义/设置/调用函数的方式是最令人困惑的部分。人们往往不会谈论它们。在指南或博客中,这不是一个强调的重点。这让我感到惊讶,因为这对大多数人来说都很困惑,而流利使用JavaScript的人也一定经历过这个过程。就像这个不被提及的禁忌现实一样,从未被谈论过。 - ahnbizcad
1
还可以阅读关于这个结构的目的或者查看一个(技术性的) 解释 (也可以在这里找到)。有关括号位置的放置,请参见关于它们位置的问题 - Bergi
OT:对于想要了解匿名函数在哪里被广泛使用的人,请阅读http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html。 - Alireza Fattahi
这是立即调用函数表达式(IIFE)的典型案例。 - Aminu Kano
10个回答

436

之所以不起作用,是因为它被解析为FunctionDeclaration,函数声明的名称标识符是强制性的

当您使用括号括起来时,它将被视为FunctionExpression进行评估,函数表达式可以有名称也可以没有。

FunctionDeclaration的语法如下:

function Identifier ( FormalParameterListopt ) { FunctionBody }

还有 FunctionExpression

function Identifieropt ( FormalParameterListopt ) { FunctionBody }

如您所见,在FunctionExpression中的 Identifier (Identifieropt) 标记是可选的,因此我们可以定义一个没有名称的函数表达式:

(function () {
    alert(2 + 2);
}());
< p > 或者 < em > 命名 函数表达式:

(function foo() {
    alert(2 + 2);
}());

圆括号(正式称为分组运算符)只能包围表达式,并且函数表达式会被评估。

这两个语法产生物可能存在歧义,并且它们看起来完全相同,例如:

function foo () {} // FunctionDeclaration

0,function foo () {} // FunctionExpression

解析器根据出现的上下文FunctionDeclaration还是FunctionExpression进行区分。

在上面的例子中,第二个函数是一个表达式,因为逗号运算符只能处理表达式。

另一方面,FunctionDeclaration实际上只能出现在称为“Program”代码的全局范围之外和其他函数的FunctionBody内部。

应避免在块内部创建函数,因为这可能导致不可预测的行为,例如:

if (true) {
  function foo() {
    alert('true');
  }
} else {
  function foo() {
    alert('false!');
  }
}

foo(); // true? false? why?

上述代码实际上应该产生一个 SyntaxError,因为 Block 只能包含语句(而 ECMAScript 规范并未定义任何函数语句),但是大多数实现都很宽容,并将简单地采用第二个函数,即警报 'false!' 的那个。

Mozilla 实现——Rhino、SpiderMonkey——有不同的行为。它们的语法包含一个 非标准 函数声明,这意味着该函数将在运行时而不是解析时进行评估,就像使用 FunctionDeclaration 时发生的那样。在这些实现中,我们将得到定义的第一个函数。


函数可以用不同的方式声明,比较以下内容

1- 使用 Function 构造函数定义的函数赋值给变量 multiply

var multiply = new Function("x", "y", "return x * y;");

2- 名为 multiply 的函数声明:

function multiply(x, y) {
    return x * y;
}

3- 一个分配给变量 multiply 的函数表达式:

var multiply = function (x, y) {
    return x * y;
};

4- 一个命名的函数表达式 func_name,被赋值给变量 multiply:

var multiply = function func_name(x, y) {
    return x * y;
};

3
CMS的回答是正确的。要深入了解函数声明与表达式,请参考kangax的这篇文章 - Tim Down
1
啊哈。非常有用。谢谢,CMS。您链接的 Mozilla 文档部分尤其启发人: https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Functions_and_function_scope#Function_constructor_vs._function_declaration_vs._function_expression - Prem
1
+1,虽然您在函数表达式中的闭合括号位置不正确 :-) - NickFitz
四年了,仍然很喜欢它...不得不查找使用分组运算符进行函数表达式的棘手转换。 - Peter Ajtai
1
@GovindRai,不会的。函数声明在编译时处理,重复的函数声明将覆盖先前的声明。在运行时,函数声明已经可用,在这种情况下,可用的是警报“false”的函数声明。有关更多信息,请阅读you don't know JS - Saurabh Misra
显示剩余4条评论

57
即使这是一个旧的问题和答案,它讨论的主题至今仍然困扰着很多开发人员。我无法计算出我面试过的JavaScript开发人员候选人中有多少人不知道函数声明和函数表达式之间的区别,也不知道立即调用的函数表达式是什么。
但是,我想提到一件非常重要的事情,即使Premasagar给它命名标识符,他的代码片段也不会起作用。
function someName() {
    alert(2 + 2);
}();

这种方法行不通的原因是JavaScript引擎将其解释为一个函数声明,后面跟着一个没有表达式的完全无关的分组运算符。而分组运算符必须包含一个表达式。根据JavaScript的规定,上面的代码片段等同于以下代码。
function someName() {
    alert(2 + 2);
}

();

我想指出的另一件事是,对于函数表达式提供的任何名称标识符,在代码上下文中除了在函数定义内部之外,基本上没有什么用处。

var a = function b() {
    // do something
};
a(); // works
b(); // doesn't work

var c = function d() {
    window.setTimeout(d, 1000); // works
};

当然,在调试代码时,使用名称标识符定义函数总是有帮助的,但那是完全不同的事情... :-)

21

已经有很好的答案了。但需要注意的是,函数声明会返回一个空的完成记录:

14.1.20 - 运行时语义:评估

FunctionDeclaration : function BindingIdentifier ( FormalParameters ) { FunctionBody }

  1. 返回 NormalCompletion(empty)。

这个事实不容易观察到,因为大多数尝试获取返回值的方法都会将函数声明转换为函数表达式。然而,eval可以显示它:

var r = eval("function f(){}");
console.log(r); // undefined

调用空的完成记录是没有意义的。这就是为什么function f(){}()不能工作的原因。实际上,JS引擎甚至不会尝试去调用它,圆括号被视为另一个语句的一部分。

但是如果您将函数包装在括号中,它就变成了函数表达式:

var r = eval("(function f(){})");
console.log(r); // function f(){}

函数表达式返回一个函数对象。因此您可以调用它:(function f(){})()


1
很遗憾这个答案被忽视了。虽然不如被接受的答案全面,但它提供了一些非常有用的额外信息,值得更多的投票。 - Avrohom Yisroel

15
在JavaScript中,这被称为立即调用函数表达式(IIFE)
为了使它成为函数表达式,您需要:
  1. 使用()将其括起来

  2. 在其前面放置void运算符

  3. 将其赋值给一个变量。

否则,它将被视为函数定义,然后您将无法通过以下方式同时调用/调用它:
 function (arg1) { console.log(arg1) }(); 

上述代码会产生错误。因为你只能立即调用函数表达式。
有几种方法可以实现这一点: 方法1:
(function(arg1, arg2){
//some code
})(var1, var2);

方法二:

(function(arg1, arg2){
//some code
}(var1, var2));

方法三:

void function(arg1, arg2){
//some code
}(var1, var2);

方法四:

  var ll = function (arg1, arg2) {
      console.log(arg1, arg2);
  }(var1, var2);

以上所有内容都将立即调用函数表达式。


3

我有一个小小的建议。只需进行一点修改,您的代码就可以正常运行:

var x = function(){
    alert(2 + 2);
}();

我使用上述语法而非更广泛流传的版本:
var module = (function(){
    alert(2 + 2);
})();

因为我没能成功地在vim中使javascript文件的缩进正常工作。看起来vim不喜欢圆括号内的花括号。


那么为什么当你将执行结果赋值给一个变量时,这个语法可以工作,但是单独使用却不行呢? - calebds
1
@paislee -- 因为JavaScript引擎将以function关键字开头的任何有效JavaScript语句解释为函数声明,在这种情况下,尾随的()被解释为分组运算符,根据JavaScript语法规则,它只能且必须包含一个JavaScript表达式。 - natlee75
2
@bosonix -- 你喜欢使用的语法很好,但为了保持一致性,最好使用你提到的“更广泛流传的版本”或者是()括起来的变体(Douglas Crockford强烈推荐),因为通常我们会使用未分配给变量的IIFE,并且如果你不始终使用它们,很容易忘记包含那些包装括号。 - natlee75

2
(function(){
     alert(2 + 2);
 })();

上面的语法是有效的,因为括号内传递的任何内容都被视为函数表达式。
function(){
    alert(2 + 2);
}();

上述语法不是有效的。因为JavaScript语法解析器在函数关键字后寻找函数名,由于它没有找到任何内容,所以会抛出一个错误。

2
虽然你的回答并不是错误的,但已经有一个被接受的答案详细地涵盖了所有这些内容。 - Till Arnold

0
也许较短的回答是这样的
function() { alert( 2 + 2 ); }

是一个函数字面量,它定义了一个(匿名)函数。额外的()对被解释为表达式,不应该在顶层使用,只能用于字面量。

(function() { alert( 2 + 2 ); })();

在一个表达式语句中调用了一个匿名函数。


0

这些额外的括号在全局命名空间和包含代码的匿名函数之间创建了额外的匿名函数。在JavaScript中,声明在其他函数内部的函数只能访问包含它们的父函数的命名空间。由于存在额外的对象(匿名函数)在全局范围和实际代码之间,因此作用域不会被保留。


0

你也可以这样使用:

! function() { console.log('yeah') }()

或者

!! function() { console.log('yeah') }()

! - 取反操作将函数定义转换为函数表达式,因此您可以立即使用()调用它。与使用0,fn defvoid fn def相同。


0

它们可以和参数-参数一起使用,例如:

var x = 3; 
var y = 4;

(function(a,b){alert(a + b)})(x,y)

将会得到7的结果


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