关于javascript:解释封装的匿名函数语法

Explain the encapsulated anonymous function syntax

总结

你能解释一下在javascript中封装匿名函数语法背后的原因吗?为什么这样做:(function(){})();,但这不起作用:function(){}();

我所知道的

在javascript中,创建这样的命名函数:

1
2
3
4
function twoPlusTwo(){
    alert(2 + 2);
}
twoPlusTwo();

还可以创建匿名函数并将其赋给变量:

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

您可以通过创建匿名函数来封装代码块,然后将其包装在括号中并立即执行:

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

这在创建模块化脚本时很有用,以避免当前作用域或全局作用域与潜在的冲突变量混淆,如GreaseMonkey脚本、jquery插件等。

现在,我明白这是为什么。括号中包含了内容,并且只公开了结果(我相信有更好的方法来描述它),例如使用(2 + 2) === 4

我不明白的是

但我不明白为什么这样做效果不一样:

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

你能给我解释一下吗?


它不起作用,因为它被解析为FunctionDeclaration,并且函数声明的名称标识符是必需的。

当用括号将其括起来时,它的计算结果为FunctionExpression,函数表达式可以命名,也可以不命名。

FunctionDeclaration的语法如下:

1
function Identifier ( FormalParameterListopt ) { FunctionBody }

FunctionExpressions:

1
function Identifieropt ( FormalParameterListopt ) { FunctionBody }

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

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

或命名函数表达式:

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

括号(正式称为分组运算符)只能环绕表达式,并计算函数表达式。

这两种语法产物可能不明确,并且看起来完全相同,例如:

1
2
3
function foo () {} // FunctionDeclaration

0,function foo () {} // FunctionExpression

解析器知道它是FunctionDeclaration还是FunctionExpression,这取决于它出现的上下文。

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

另一方面,FunctionDeclaration实际上只能出现在所谓的"Program代码中,这意味着代码在全球范围之外,在其他功能的FunctionBody内。

应避免使用块内的函数,因为它们可能导致不可预测的行为,例如:

1
2
3
4
5
6
7
8
9
10
11
if (true) {
  function foo() {
    alert('true');
  }
} else {
  function foo() {
    alert('false!');
  }
}

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

上面的代码实际上应该生成一个SyntaxError,因为Block只能包含语句(而ecmascript规范没有定义任何函数语句),但是大多数实现都是可容忍的,并且只接受第二个函数,即提醒'false!'的函数。

Mozilla实现——Rhino、Spidermonkey——有不同的行为。它们的语法包含一个非标准的函数语句,这意味着函数将在运行时(而不是解析时)进行计算,就像在FunctionDeclaration中那样。在这些实现中,我们将定义第一个函数。

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

1-用分配给变量乘法的函数构造函数定义的函数:

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

2-名为乘法的函数的函数声明:

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

3-分配给变量乘法的函数表达式:

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

4-指定给变量乘法的命名函数表达式func_name:

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


尽管这是一个古老的问题和答案,但它讨论了一个至今仍让许多开发人员陷入循环的主题。我无法计算我采访过的那些不能告诉我函数声明和函数表达式之间的区别,也不知道立即调用的函数表达式是什么的javascript开发人员候选人的数量。

不过,我想提一件非常重要的事情,那就是,即使premasagar给了它一个名称标识符,它的代码片段也不会工作。

1
2
3
function someName() {
    alert(2 + 2);
}();

这不起作用的原因是JavaScript引擎将其解释为一个函数声明,后面跟一个完全不相关的分组运算符,该分组运算符不包含表达式,并且分组运算符必须包含表达式。根据javascript,上面的代码片段相当于下面的代码片段。

1
2
3
4
5
function someName() {
    alert(2 + 2);
}

();

我想指出的另一件事可能对某些人有用,那就是在代码上下文中,除了函数定义本身之外,为函数表达式提供的任何名称标识符几乎都是无用的。

1
2
3
4
5
6
7
8
9
var a = function b() {
    // do something
};
a(); // works
b(); // doesn't work

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

当然,在调试代码时,在函数定义中使用名称标识符总是很有帮助的,但这完全是另外一回事…-)


伟大的答案已经发布。但我要注意的是,函数声明返回一个空的完成记录:

14.1.20 - Runtime Semantics: Evaluation

FunctionDeclaration : function BindingIdentifier ( FormalParameters ) { FunctionBody }

  • Return NormalCompletion(empty).
  • 这个事实不容易观察到,因为尝试获取返回值的大多数方法都会将函数声明转换为函数表达式。但是,eval显示:

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

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

    但如果将函数括在括号中,它将成为一个函数表达式:

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

    函数表达式返回一个函数对象。因此你可以称之为:(function f(){})()


    在JavaScript中,这称为立即调用的函数表达式(IIFE)。

    要使其成为函数表达式,必须:

  • 使用()将其括起来

  • 在它前面放置一个空操作符

  • 将它赋给变量。

  • 否则,它将被视为函数定义,然后您将无法通过以下方式同时调用/调用它:

    1
     function (arg1) { console.log(arg1) }();

    以上将给您带来错误。因为您只能立即调用函数表达式。

    这可以通过以下两种方式实现:方式1:

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

    方式2:

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

    方式3:

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

    方式4:

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

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


    我还有一句话要说。您的代码只需稍作更改即可使用:

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

    我使用上述语法而不是更广泛的版本:

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

    因为我没能让缩进正确地用于VIM中的javascript文件。似乎Vim不喜欢左括号内的花括号。


    您也可以像这样使用它:

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

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

    !negation op将fn定义转换为fn表达式,因此,可以使用()立即调用它。与使用0,fn defvoid fn def相同


    这些额外的括号在全局命名空间和包含代码的匿名函数之间创建额外的匿名函数。在javascript中,在其他函数中声明的函数只能访问包含它们的父函数的命名空间。因为在全局范围和实际代码范围之间有额外的对象(匿名函数),所以不会保留范围。


    它们可以与参数参数一起使用,例如

    1
    2
    3
    4
    var x = 3;
    var y = 4;

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

    结果是7


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

    以上是有效的语法,因为括号内传递的任何内容都被视为函数表达式。

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

    上面的语法无效。因为Java脚本语法分析器在函数关键字后面查找函数名,因为它找不到任何东西,所以它抛出了一个错误。


    也许更短的答案是

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

    是定义(匿名)函数的函数文本。附加的()-对(解释为表达式)不应出现在顶层,只应出现文本。

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

    在调用匿名函数的表达式语句中。