为什么这个函数用括号包裹起来,然后再跟着另一组括号?

43

我经常在JavaScript源代码中看到这种写法,但我从未真正了解为什么要使用这种结构。这有什么必要吗?

(function() {

    //stuff

})();

为什么要这样写?为什么不直接使用stuff而不是在函数中使用它?

编辑:我知道这是定义一个匿名函数然后调用它,但是为什么


3
+1 对于我长期以来想知道的事情。希望得到一个有教养的答案! - F.P
6个回答

33

这定义了一个函数闭包

它用于创建一个带有私有功能和变量的函数闭包,这些功能和变量不是全局可见的。

考虑以下代码:

(function(){
    var test = true;
})();

变量test只在函数闭包内部可见,而在其他地方是不可见的。

什么是闭包?

函数闭包使得各种脚本之间不会相互干扰,即使它们定义了同名的变量或私有函数。这些私有内容仅在闭包内部可见和访问,而在闭包外部则无法访问。

请查看以下代码并阅读其注释:

// public part
var publicVar = 111;
var publicFunc = function(value) { alert(value); };
var publicObject = {
    // no functions whatsoever
};

    // closure part
    (function(pubObj){
        // private variables and functions
        var closureVar = 222;
        var closureFunc = function(value){
            // call public func
            publicFunc(value);
            // alert private variable
            alert(closureVar);
        };

        // add function to public object that accesses private functionality
        pubObj.alertValues = closureFunc;

        // mind the missing "var" which makes it a public variable
        anotherPublic = 333;

    })(publicObject);

// alert 111 & alert 222
publicObject.alertValues(publicVar);

// try to access varaibles
alert(publicVar); // alert 111
alert(anotherPublic); // alert 333
alert(typeof(closureVar)); // alert "undefined"

这里有一个JSFiddle运行代码,根据上面的注释显示数据。

它实际上是做什么的?

正如你已经知道的那样,它:

  1. 创建一个函数:

    function() { ... }
    
    并立即执行它:
  2. (func)();
    
  3. 这个函数可能会或可能不会接受额外的参数。

通常,jQuery 插件是通过定义一个带有一个参数的函数来实现的,插件在其中进行操作:

(function(paramName){ ... })(jQuery);

但主要思想仍然相同:使用私有定义定义一个函数闭包,该闭包不能直接在外部使用。


17

这种构造被称为自执行匿名函数,其实并不是一个很好的名字,下面是它的运作方式(以及为什么这个名字不太好)。这样做:

function abc() {
    //stuff
}

定义一个名为abc的函数,如果我们想要一个匿名函数(这是JavaScript中非常常见的一种模式),它可能会是以下内容:

function() {
    //stuff
}

但是,如果你有这个匿名函数,你要么需要将其与一个变量关联起来,这样你就可以调用它(这将使其不那么匿名),要么你需要立即执行它。我们可以通过以下方式尝试立即执行它:

function() {
    //stuff
}();

但这样做将无法起作用,因为它会导致语法错误。你得到语法错误的原因是这样的。当你创建一个带名称的函数(比如上面的abc),该名称变成了对函数表达式的引用,你可以通过在名称后加上 () 来执行表达式,例如:abc()。声明函数本身不会创建表达式,函数声明实际上是一个语句而不是表达式。本质上,表达式是可执行的,而语句则不是(您可能已经猜到了)。因此,为了执行匿名函数,您需要告诉解析器它是一个表达式而不是语句。一种方法(不是唯一的方法,但已成为惯例)是将您的匿名函数包装在一组 () 中,因此您得到以下结构:

(function() {
    //stuff
})();

一个立即执行的匿名函数(可以看出这个构造名称有点不准确,因为它并不是一个自我执行的匿名函数,而是一个立即执行的匿名函数)。

那么为什么这些都很有用呢?一个原因是它可以防止代码污染全局命名空间。由于 JavaScript 中的函数具有它们自己的作用域,函数内部的任何变量都不会在全局范围内可见,因此如果我们能够以某种方式将所有代码都写在一个函数内部,全局作用域将是安全的,而我们的自执行匿名函数正好可以实现这一点。让我借用 John Resig 老书中的一个例子:

// Create a new anonymous function, to use as a wrapper
(function(){
  // The variable that would, normally, be global
  var msg = "Thanks for visiting!";
  // Binding a new function to a global object
  window.onunload = function(){
    // Which uses the 'hidden' variable
    alert( msg );
  };
  // Close off the anonymous function and execute it
})();

我们所有的变量和函数都是写在自执行匿名函数内部的,因为它在自执行匿名函数内部,所以我们的代码首先被执行。由于JavaScript允许闭包,也就是允许函数访问在外部函数定义的变量,我们几乎可以在自执行匿名函数内部编写任何代码,并且一切都会按预期工作。

但等等,这里还有更多的好处 :). 这种结构允许我们解决使用JavaScript中闭包时经常会出现的问题。我再次引用John Resig的话:

请记住,闭包允许您引用存在于父函数中的变量。但是,它不提供创建该变量时的值;它提供父函数内变量的最后一个值。您将看到这种情况最常见的问题发生在for循环中。有一个变量被用作迭代器(例如i)。在for循环内部,正在创建使用闭包引用迭代器的新函数。问题在于,当调用新闭合函数时,它们将引用迭代器的最后一个值(即数组中的最后一个位置),而不是您期望的值。图2-16显示了使用匿名函数诱导范围的示例,创建实例,其中可能存在预期的闭包。

// An element with an ID of main
var obj = document.getElementById("main");

// An array of items to bind to
var items = [ "click", "keypress" ];

// Iterate through each of the items
for ( var i = 0; i < items.length; i++ ) {
  // Use a self-executed anonymous function to induce scope
  (function(){
    // Remember the value within this scope
    var item = items[i];
    // Bind a function to the element
    obj[ "on" + item ] = function() {
      // item refers to a parent variable that has been successfully
      // scoped within the context of this for loop
      alert( "Thanks for your " + item );
    };
  })();
}

简而言之,所有这些都意味着,人们经常编写像这样的天真的javascript代码(这是上面循环的天真版本):

for ( var i = 0; i < items.length; i++ ) {
    var item = items[i];
    // Bind a function to the elment
    obj[ "on" + item ] = function() {
      alert( "Thanks for your " + items[i] );
    };
}

我们在循环中创建的函数是闭包,但不幸的是它们会锁定外部作用域中 i最后一个值(在这种情况下,它可能是2,这会引起问题)。我们想要的可能是每个在循环内创建的函数都锁定我们创建时的i值。这就是我们自执行匿名函数的用处,下面是一种类似但更易理解的重写循环的方式:

for ( var i = 0; i < items.length; i++ ) {
  (function(index){
    obj[ "on" + item ] = function() {
      alert( "Thanks for your " + items[index] );
    };
  })(i);
}
因为我们在每次迭代时调用匿名函数,所以我们传递的参数被锁定为传递时的值,因此我们在循环内创建的所有函数都将按预期工作。
以上是使用自执行匿名函数结构的两个好理由,也是它为什么能够正常工作的原因。

实际上还有第三个原因,与不污染全局命名空间有些关联,但本质上允许您创建具有私有变量的对象。如果有人感兴趣,我可以提供更多细节。 - skorks

3

这个用法是定义一个匿名函数并立即调用它。我猜在代码块周围加括号的原因是JavaScript需要它们来理解函数调用。

如果你想在现场定义一个一次性函数,然后立即调用它,这个用法非常有用。使用匿名函数和直接编写代码之间的区别在于作用域。所有匿名函数中的变量都会在函数结束时失去作用域(除非变量另有说明)。这可以用于保持全局或封闭命名空间的清洁,长期使用更少的内存,或获得一些“隐私”。


1
问题是为什么他们这样做,而不是它的作用是什么。 - Rufinus
@Rufinus 如果您继续阅读,我在我的回答中提供了一个相当详细的想法。请查看。 - Rafe Kettler
当我评论时,这个内容还不存在 :) - Rufinus
@Rufinus 的编辑比较棘手。我的回答是在 OP 编辑之前发布的。 - Rafe Kettler

1

这是一个“匿名自执行函数”或“立即调用函数表达式”。Ben Alman在这里给出了很好的解释。

我在创建命名空间时使用这种模式。

var APP = {};

(function(context){



})(APP);

为什么要将APP变量传回函数中? - Doug Hauf
将值锁定到闭包中。 - redsquare
当你说要将值锁定回封闭器时,你的意思是将值锁定回使用函数(context) { }创建的命名空间中,对吗? - Doug Hauf

1

这种结构在编程中非常有用,特别是当您想要创建闭包时 - 该结构可帮助创建一个专门的“房间”,使变量无法从外部访问。详见“JavaScript精粹”一书中的这一章节: http://books.google.com/books?id=PXa2bby0oQ0C&pg=PA37&lpg=PA37&dq=crockford+closure+called+immediately&source=bl&ots=HIlku8x4jL&sig=-T-T0jTmf7_p_6twzaCq5_5aj3A&hl=lv&ei=lSa5TaXeDMyRswa874nrAw&sa=X&oi=book_result&ct=result&resnum=1&ved=0CBUQ6AEwAA#v=onepage&q&f=false

在第38页顶部显示的示例中,您可以看到变量“status”被隐藏在闭包内,除了调用get_status()方法外无法访问。

这本参考书中的解释非常好。这是我一直想读的一本书。 - Gary Willoughby
@Gary:这篇文章比理解这个概念所需的长度和细节要多得多。 - Robert Koritnik

1

我不确定这个问题是否已经被回答了,如果我只是在重复的话,请原谅。

在JavaScript中,只有函数引入新的作用域。通过将代码包装在立即函数中,您定义的所有变量仅存在于此或更低的作用域中,但不在全局作用域中

因此,这是一个不污染全局作用域的好方法。

应该只有很少的全局变量。请记住,每个全局变量都是window对象的属性,该对象默认已经有很多属性。引入新的作用域也可以避免与window对象的默认属性发生冲突。


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