JavaScript闭包与匿名函数

时间:2012-10-17 08:36:36

标签: javascript scope closures

我的一位朋友和我正在讨论什么是JS的封闭,什么不是。我们只是想确保我们真正理解它。

我们来看这个例子。我们有一个计数循环,并希望在控制台上打印计数器变量延迟。因此,我们使用setTimeout闭包来捕获计数器变量的值,以确保它不会打印N次N值。

没有关闭或接近关闭的错误解决方案将是:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

当然会在循环后打印10倍i的值,即10。

所以他的尝试是:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

按预期打印0到9。

我告诉他,他并没有使用封闭来捕获i,但他坚持认为他是。{1}}。我通过将for循环体放在另一个setTimeout(将他的匿名函数传递给setTimeout),再次打印10次10​​来证明他没有使用闭包。如果我将他的函数存储在var并在循环之后执行,同样打印10次10​​次也是如此。所以我的论点是他不是真的捕获 i 的值,使其版本成为闭包。

我的尝试是:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

所以我捕获i(在闭包中命名为i2),但现在我返回另一个函数并传递它。 在我的情况下,传递给setTimeout的函数实际上捕获了i

现在谁在使用闭包,谁不使用?

请注意,两个解决方案在控制台上打印0到9都会延迟,因此它们可以解决原始问题,但我们想要了解这两个解决方案中哪一个使用闭包来实现此目的。

11 个答案:

答案 0 :(得分:631)

编辑注: JavaScript中的所有函数都是闭包,如post中所述。然而,我们只想从理论的角度确定interesting这些函数的子集。此后,除非另有说明,否则对 closure 一词的任何引用都将引用此函数子集。

关闭的简单解释:

  1. 采取一项功能。我们称之为F。
  2. 列出F。
  3. 的所有变量
  4. 变量可能有两种类型:
    1. 局部变量(绑定变量)
    2. 非局部变量(自由变量)
  5. 如果F没有自由变量,则它不能成为闭包。
  6. 如果F有任何自由变量(在 a F的父范围中定义),则:
    1. 必须只有一个F的父范围, a 自由变量才会被绑定。
    2. 如果F 引用来自那个父作用域,那么它就会成为那个自由变量的闭包。
    3. 自由变量称为闭包F的上升值。
  7. 现在让我们用它来弄清楚谁使用了闭包,谁没有使用(为了解释我已经命名了函数):

    案例1:您朋友的计划

    for (var i = 0; i < 10; i++) {
        (function f() {
            var i2 = i;
            setTimeout(function g() {
                console.log(i2);
            }, 1000);
        })();
    }
    

    在上述程序中,有两个功能:fg。让我们看看它们是否是封闭的:

    f

    1. 列出变量:
      1. i2本地变量。
      2. i是一个免费变量。
      3. setTimeout是一个免费变量。
      4. g本地变量。
      5. console是一个免费变量。
    2. 找到每个自由变量绑定的父范围:
      1. i 绑定到全局范围。
      2. setTimeout 绑定到全局范围。
      3. console 绑定到全局范围。
    3. 功能引用的范围是什么? 全球范围
      1. 因此i f 关闭
      2. 因此setTimeout f 关闭
      3. 因此console f 关闭
    4. 因此函数f不是闭包。

      g

      1. 列出变量:
        1. console是一个免费变量。
        2. i2是一个免费变量。
      2. 找到每个自由变量绑定的父范围:
        1. console 绑定到全局范围。
        2. i2 绑定f的范围。
      3. 功能引用的范围是什么? {strong}范围setTimeout
        1. 因此console g 关闭
        2. 因此i2 被<{1}} 关闭
      4. 因此,函数g是自由变量g的闭包(这是i2的上升值) > g内的

        对你不好:你的朋友正在使用一个闭包。内部函数是一个闭包。

        案例2:您的计划

        setTimeout

        在上述程序中,有两个功能:for (var i = 0; i < 10; i++) { setTimeout((function f(i2) { return function g() { console.log(i2); }; })(i), 1000); } f。让我们看看它们是否是封闭的:

        g

        1. 列出变量:
          1. f本地变量。
          2. i2本地变量。
          3. g是一个免费变量。
        2. 找到每个自由变量绑定的父范围:
          1. console 绑定到全局范围。
        3. 功能引用的范围是什么? 全球范围
          1. 因此console console 关闭
        4. 因此函数f不是闭包。

          f

          1. 列出变量:
            1. g是一个免费变量。
            2. console是一个免费变量。
          2. 找到每个自由变量绑定的父范围:
            1. i2 绑定到全局范围。
            2. console 绑定i2的范围。
          3. 功能引用的范围是什么? {strong}范围f
            1. 因此setTimeout console 关闭
            2. 因此g 被<{1}} 关闭
          4. 因此,函数i2是自由变量g的闭包(这是g的上升值) > i2内的

            对你有好处:你正在使用一个闭包。内部函数是一个闭包。

            所以你和你的朋友都在使用闭包。停止争论。我希望我清除了闭包的概念,以及如何为你们两个识别它们。

            编辑:关于为什么所有功能都关闭的简单解释(来自@Peter):

            首先让我们考虑以下程序(它是control):

            &#13;
            &#13;
            g
            &#13;
            &#13;
            &#13;

            1. 我们知道setTimeoutlexicalScope(); function lexicalScope() { var message = "This is the control. You should be able to see this message being alerted."; regularFunction(); function regularFunction() { alert(eval("message")); } }关闭来自上述定义
            2. 当我们执行程序时,我们希望 lexicalScope被警告,因为 regularFunction不是关闭(即它可以访问所有其父范围内的变量 - 包括message)。
            3. 当我们执行我们观察的程序时,regularFunction确实已被警告。
            4. 接下来让我们考虑以下程序(它是alternative):

              &#13;
              &#13;
              message
              &#13;
              &#13;
              &#13;

              1. 我们知道只有message是来自上述定义的结束
              2. 当我们执行程序时,我们希望 var closureFunction = lexicalScope(); closureFunction(); function lexicalScope() { var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure."; return function closureFunction() { alert(eval("message")); }; }不会被警告,因为 closureFunction是一个闭包(即它只能访问所有 创建函数时的非局部变量 see this answer) - 这不包括message)。
              3. 当我们执行我们观察的程序时,closureFunction实际上已被警告。
              4. 我们从中推断出什么?

                1. JavaScript解释器不会将闭包与处理其他函数的方式区别对待。
                2. 每个函数都随身携带scope chain。闭包没有单独的引用环境。
                3. 闭包就像其他所有功能一样。我们只是在引用的范围引用时将它们称为闭包>它们所属的范围因为这是一个有趣的案例。

答案 1 :(得分:94)

根据closure定义:

  

“闭包”是一个表达式(通常是一个函数),它可以包含自由变量以及绑定这些变量的环境(“关闭”表达式)

如果您定义了一个使用在函数外部定义的变量的函数,则使用closure。 (我们将变量称为自由变量) 他们都使用closure(即使在第一个例子中)。

答案 2 :(得分:49)

简而言之 Javascript Closures 允许函数访问在词法 - 父函数中声明的变量

让我们看一个更详细的解释。 要理解闭包,了解JavaScript如何定义变量非常重要。

<强>作用域

在JavaScript中,范围是使用函数定义的。 每个函数都定义了一个新的范围。

考虑以下示例;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

调用f打印

hello
hello
2
Am I Accessible?

现在让我们考虑我们在另一个函数g中定义函数f的情况。

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

我们会将f g词汇称为f。 如前所述,我们现在有两个范围;范围g和范围g

但是一个范围是“在”另一个范围内,那么父函数范围的子函数范围是什么?在父函数范围内声明的变量会发生什么?我能从子功能的范围访问它们吗? 这正是关闭步骤的地方。

<强>闭包

在JavaScript中,函数g不仅可以访问范围f中声明的任何变量,还可以访问在父函数function f()//lexical parent function {//begin of scope f var foo='hello'; //foo declared in scope f function g() {//being of scope g var bar='bla'; //bar declared in scope g console.log(foo); }//end of scope g g(); console.log(bar); }//end of scope f 范围内声明的任何变量。

考虑以下事项;

hello
undefined

调用f打印

console.log(foo);

让我们看一下g行。此时我们在范围foo中,我们尝试访问范围f中声明的变量g。但是如前所述,我们可以访问词法父函数中声明的任何变量,这就是这里的情况; fhello的词汇父级。因此会打印console.log(bar); 我们现在看一下f行。此时我们在范围bar中,我们尝试访问范围g中声明的变量barg未在当前范围内声明,而f函数不是bar的父级,因此h未定义为

实际上我们也可以访问在词法“祖父”函数范围内声明的变量。因此,如果在函数g

中定义了函数function f() {//begin of scope f function g() {//being of scope g function h() {//being of scope h /*...*/ }//end of scope h /*...*/ }//end of scope g /*...*/ }//end of scope f
h

然后h将能够访问在函数gf scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... 范围内声明的所有变量。这是通过闭包完成的。在JavaScript中闭包允许我们访问词法父函数,词法祖父函数,词汇祖父函数等中声明的任何变量。 这可以看作是范围链; window直到最后一个没有词法父项的父函数。

窗口对象

实际上链不会停在最后一个父函数上。还有一个特殊的范围; 全球范围。未在函数中声明的每个变量都被视为在全局范围内声明。全球范围有两个专业;

  • 在全局范围内声明的每个变量都可以访问无处不在
  • 在全局范围内声明的变量对应于foo对象的属性。

因此,在全局范围内有两种方式声明变量foo;通过不在函数中声明它或通过设置窗口对象的属性var foo = 'hello'; function f(){console.log(foo)}; f(); //JavaScript-No-Closure prints undefined //JavaSript prints hello

两次尝试都使用闭包

现在您已经阅读了更详细的解释,现在很明显两个解决方案都使用了闭包。 但可以肯定的是,让我们做一个证明。

让我们创建一种新的编程语言; JavaScript的无闭幕。 顾名思义,JavaScript-No-Closure与JavaScript完全相同,只是它不支持闭包。

换句话说;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

好的,让我们看看使用JavaScript-No-Closure的第一个解决方案会发生什么;

undefined

因此,这将在JavaScript-No-Closure中打印for(var i = 0; i < 10; i++) { setTimeout((function(i2){ return function() { console.log(i2); //i2 is undefined in JavaScript-No-Closure } })(i), 1000); } 10次。

因此,第一个解决方案使用闭包。

让我们看看第二个解决方案;

undefined

因此,这将在JavaScript-No-Closure中打印foo 10次。

两种解决方案都使用闭包。

编辑:假设这3个代码片段未在全局范围内定义。否则,变量iwindow将绑定到window对象,因此可以通过JavaScript和JavaScript-No-Closure中的{{1}}对象访问。

答案 3 :(得分:21)

我从未对任何人解释这一点的方式感到满意。

理解闭包的关键是理解没有闭包的JS会是什么样的。

没有闭包,这会引发错误

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

一旦outerFunc在一个假想的关闭禁用版本的JavaScript中返回,对outerVar的引用将被垃圾收集并且不再留下任何内容以供引用的内部函数。

闭包本质上是特殊规则,当内部函数引用外部函数的变量时,这些规则可以使这些变量存在。使用闭包时,即使在外部函数完成后也会保持引用的变量,如果这有助于您记住该点,则将其“关闭”。

即使使用闭包,在没有内部函数引用其本地函数的函数中的局部变量的生命周期与无闭包版本中的函数相同。当函数完成后,本地人就会收集垃圾。

一旦你在内部函数中引用了一个外部变量,但它就像一个门框被放入了那些引用变量的垃圾收集方式。

一种看待闭包的更准确的方法是,内部函数基本上使用内部作用域作为自己的作用域。

但是引用的上下文实际上是持久的,而不是像快照。重复触发返回的内部函数,该函数不断递增并记录外部函数的局部变量将继续警告更高的值。

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

答案 4 :(得分:16)

你们都在使用闭包。

我要去Wikipedia definition这里:

  

在计算机科学中,闭包(也是词汇封闭或功能   closure)是一个函数或函数的引用   引用环境 - 存储对每个引用的引用的表   该函数的非局部变量(也称为自由变量)。   闭包 - 与普通函数指针不同 - 允许函数访问   那些非局部变量,即使在其直接调用之外也是如此   词汇范围。

您朋友的尝试通过获取其值并制作副本以存储到本地i,明确使用非本地变量i2

您自己的尝试将i(在调用网站的范围内)传递给匿名函数作为参数。到目前为止,这不是一个闭包,但是该函数返回另一个引用相同i2的函数。由于内部匿名函数i2不是本地函数,因此会创建一个闭包。

答案 5 :(得分:12)

你和你的朋友都使用闭包:

  

闭包是一种特殊的对象,它结合了两个东西:一个函数,以及创建该函数的环境。环境由创建闭包时在范围内的任何局部变量组成。

     

MDN:https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

在朋友的代码函数function(){ console.log(i2); }中定义了匿名函数function(){ var i2 = i; ...的闭包,并且可以读/写局部变量 i2

在函数function(){ console.log(i2); }的闭包内定义的代码函数function(i2){ return ...中,可以读取/写入本地有价值的 i2 (在本例中声明为参数)

在两种情况下,函数function(){ console.log(i2); }都会传递到setTimeout

另一个等价物(但内存利用率较低)是:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}

答案 6 :(得分:9)

让我们看看两种方式:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

声明并立即执行在其自己的上下文中运行setTimeout()的匿名函数。通过先复制i来保留i2的当前值;它起作用是因为立即执行。

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

声明内部函数的执行上下文,其中i的当前值保留为i2;这种方法也使用立即执行来保留价值。

重要

应该提到的是,两种方法之间的运行语义并不相同;你的内部函数传递给setTimeout(),而他的内部函数本身调用setTimeout()

将两个代码包装在另一个setTimeout()内并不能证明只有第二种方法使用了闭包,开始时就不一样了。

<强>结论

这两种方法都使用闭合,因此它归结为个人品味;第二种方法更容易“移动”或概括。

答案 7 :(得分:9)

<强>封闭

闭包不是函数,也不是表达式。它必须被视为函数内部使用的变量的一种“快照”,并在函数内部使用。在语法上,人们应该说:'关闭变量'。

再次,换句话说:闭包是函数所依赖的变量的相关上下文的副本。

再一次(naïf):一个闭包可以访问未作为参数传递的变量。

请记住,这些功能概念在很大程度上取决于您使用的编程语言/环境。在JavaScript中,闭包取决于词法范围(在大多数c语言中都是如此)。

因此,返回一个函数主要是返回一个匿名/未命名的函数。当函数访问变量时,不作为参数传递,并且在其(词法)范围内,已经采用了闭包。

所以,关于你的例子:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

所有人都在使用闭包。不要将执行点与闭包混淆。如果闭包的'快照'是在错误的时刻拍摄的,那么这些值可能是意料之外的,但肯定会关闭!

答案 8 :(得分:7)

我刚才写了这篇文章,提醒自己一个闭包是什么以及它在JS中是如何工作的。

闭包是一个函数,当被调用时,它使用声明它的作用域,而不是它被调用的作用域。在javaScript中,所有函数的行为都是这样的。只要存在仍指向它们的函数,范围中的变量值就会持续存在。规则的例外是'this',它指的是调用函数时函数所在的对象。

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 

答案 9 :(得分:5)

仔细检查后,看起来你们两个都在使用闭合。

在您的朋友案例中,i在匿名函数1中被访问,i2在匿名函数2中被访问,其中console.log存在。

在您的情况下,您正在匿名函数中访问i2,其中console.log存在。在debugger;之前添加console.log语句,在“范围变量”下的chrome开发人员工具中添加{{1}}语句,它将告知变量的范围。

答案 10 :(得分:3)

请考虑以下事项。 这会创建并重新创建一个在f上关闭但不同的函数i!:

&#13;
&#13;
i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));
&#13;
&#13;
&#13;

而以下关闭&#34; a&#34;功能&#34;本身&#34;
(他们自己!之后的片段使用单个指示物f

&#13;
&#13;
for(var i = 0; i < 10; i++) {
    setTimeout( new Function('console.log('+i+')'),  1000 );
}
&#13;
&#13;
&#13;

或更明确:

&#13;
&#13;
for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}
&#13;
&#13;
&#13;

NB。在 f打印之前,function(){ console.log(9) }的最后定义为0

买者!封闭概念可能是对初级编程本质的强制分心:

&#13;
&#13;
for(var i = 0; i < 10; i++) {     setTimeout( 'console.log('+i+')',  1000 );      }
&#13;
&#13;
&#13;

的x参考文献:
How do JavaScript closures work?
Javascript Closures Explanation
Does a (JS) Closure Require a Function Inside a Function
How to understand closures in Javascript?
Javascript local and global variable confusion