JavaScript闭包vs.匿名函数

我的一个朋友和我正在讨论什么是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,但他坚持说他是。我通过将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),但现在我使用了返回另一个函数并传递它。# EYZ4

现在谁在使用闭包,谁没有呢?

请注意,这两个解决方案都延迟在控制台上打印0到9,因此它们解决了最初的问题,但我们想了解这两个解决方案中哪一个使用闭包能够完成这个任务。

119257 次浏览

在仔细检查之后,看起来你们都在使用封闭。

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

在您的情况下,您正在匿名函数中访问i2,其中console.log存在。在console.log之前添加debugger;语句,在chrome开发工具的“作用域变量”下,它会告诉变量的作用域是什么。

根据closure定义:

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

如果你定义的函数使用了函数外部定义的变量,则使用closure。(我们称变量为a 自由变量).
它们都使用closure(甚至在第一个例子中)

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

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

MDN: # EYZ0

在你朋友的代码中,函数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);
}

你们都在使用闭包。

我在这里使用维基百科的定义:

在计算机科学中,闭包(也称为词法闭包或函数) 闭包)是一个函数或对函数的引用 引用环境—存储对每个类的引用的表 该函数的非局部变量(也称为自由变量)。 闭包(与普通函数指针不同)允许函数进行访问 这些非局部变量即使在其immediate之外调用 词法范围。< / p >

您朋友的尝试显然使用了非本地变量i,方法是获取其值并复制到本地的i2中。

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

让我们看看这两种方式:

(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()中并不能证明只有第二种方法使用闭包,只是从一开始就不一样。

结论

这两种方法都使用闭包,所以这取决于个人喜好;第二种方法更容易“移动”或泛化。

简而言之,Javascript闭包允许访问变量的函数为在词法父函数中声明

让我们来看看更详细的解释。 要理解闭包,重要的是要理解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?

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

function f()
{//begin of scope f
function g()
{//being of scope g
/*...*/
}//end of scope g
/*...*/
}//end of scope f
我们将f称为g词法父。 如前所述,我们现在有两个范围;范围f和范围g.

但是一个作用域“在”另一个作用域中,那么子函数的作用域是父函数作用域的一部分吗?在父函数的作用域中声明的变量会发生什么;我是否能够从子函数的作用域访问它们?

闭包

在JavaScript中,函数g不仅可以访问作用域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

调用f打印

hello
undefined
让我们看看行console.log(foo);。此时,我们在作用域g中,我们试图访问在作用域f中声明的变量foo。但如前所述,我们可以访问在词法父函数中声明的任何变量这里就是这种情况;gf的词法父。因此,hello被打印出来 现在让我们看看行console.log(bar);。此时,我们在作用域f中,我们试图访问在作用域g中声明的变量barbar没有在当前作用域中声明,g也不是f的父函数,因此bar没有定义

实际上,我们还可以访问词法“grandparent”函数作用域中声明的变量。因此,如果在函数g中定义了一个函数h

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将能够访问函数hgf范围内声明的所有变量。这是用闭包完成的。在JavaScript中,闭包允许我们访问在词法父函数、词法大父函数、词法大-大父函数中声明的任何变量,等等。 这可以被看作是作用域链; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... 直到最后一个没有词法父函数的父函数

窗口对象

实际上,这个链并不止于最后一个父函数。还有一个更特殊的范围;# EYZ0。每个没有在函数中声明的变量都被认为是在全局作用域中声明的。全球范围有两个专业;

  • 在全局作用域中声明的每个变量都可以访问到处都是
  • 在全局作用域中声明的变量对应于window对象的属性。

因此,在全局作用域中声明变量foo有两种方法;要么不在函数中声明它,要么设置窗口对象的foo属性。

两种尝试都使用闭包

现在您已经阅读了更详细的解释,现在可能很明显,这两个解决方案都使用闭包。 但为了确定,我们来做一个证明 让我们创建一个新的编程语言;JavaScript-No-Closure。 顾名思义,JavaScript- no - closure和JavaScript是一样的,只是它不支持闭包

换句话说;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

好吧,让我们看看javascript的第一个解决方案- no - closure会发生什么;

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中打印undefined 10次-无闭包。

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

让我们看看第二个解;

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

因此,这将在javascript中打印undefined 10次-无闭包。

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

编辑:假设这3个代码片段没有在全局作用域中定义。否则,变量fooi将绑定到window对象,因此可以通过JavaScript和JavaScript- no - closure中的window对象访问。

关闭

闭包不是函数,也不是表达式。它必须被视为一种从函数作用域外使用的变量到函数内部使用的“快照”。从语法上讲,我们应该说:“取变量的闭包”。

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

再说一次(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)
}

所有都使用闭包。不要将执行点与闭包混淆。如果闭包的“快照”是在错误的时刻拍摄的,值可能是意外的,但肯定是一个闭包!

JavaScript中的所有函数都是闭包,这在帖子中解释过。然而,我们只对这些函数的一个子集感兴趣,从理论的角度来看,它们是有趣的。从今以后,除非另有说明,任何对关闭这个词的引用都将指这个函数子集。

闭包的一个简单解释:

  1. 取一个函数。我们称它为F。
  2. 列出F的所有变量。
  3. 变量可以是两种类型:
    1. 局部变量(绑定变量)
    2. 非局部变量(自由变量)
    3. 李< / ol > < / >
    4. 如果F没有自由变量,那么它就不是一个闭包。
    5. 如果F有任何自由变量(在F的一个父作用域中定义),则:
      1. 一个自由变量只能绑定到F的一个父作用域。
      2. 如果F在父作用域外为引用,则它成为自由变量的闭包。
      3. 自由变量被称为闭包F的上值。
      4. 李< / ol > < / >

      现在让我们用这个来弄清楚谁使用闭包,谁不使用(为了解释,我已经命名了函数):

      案例1:你朋友的程序

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

      在上面的程序中有两个函数:fg。让我们看看它们是否是闭包:

      # EYZ0:

        列出变量:
        1. i2是一个当地的变量。
        2. i是一个免费的变量。
        3. setTimeout是一个免费的变量。
        4. g是一个当地的变量。
        5. console是一个免费的变量。
        6. 李< / ol > < / >
        7. 找到每个自由变量所绑定的父作用域:
          1. i对于全局作用域是绑定
          2. setTimeout对于全局作用域是绑定
          3. console对于全局作用域是绑定
          4. 李< / ol > < / > 函数引用在哪个作用域?# EYZ1。
            1. 因此i不是关闭了f
            2. 因此setTimeout不是关闭了f
            3. 因此console不是关闭了f
            4. 李< / ol > < / >

            因此,函数f不是一个闭包。

            # EYZ0:

              列出变量:
              1. console是一个免费的变量。
              2. i2是一个免费的变量。
              3. 李< / ol > < / >
              4. 找到每个自由变量所绑定的父作用域:
                1. console对于全局作用域是绑定
                2. i2绑定f的范围。
                3. 李< / ol > < / > 函数引用在哪个作用域?# EYZ2。
                  1. 因此console不是关闭了g
                  2. 因此i2 = 关闭了 * g
                  3. 李< / ol > < / >

                  因此,函数g是自由变量i2(它是g的一个向上值),它是setTimeout中的引用

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

                  案例2:您的程序

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

                  在上面的程序中有两个函数:fg。让我们看看它们是否是闭包:

                  # EYZ0:

                    列出变量:
                    1. i2是一个当地的变量。
                    2. g是一个当地的变量。
                    3. console是一个免费的变量。
                    4. 李< / ol > < / >
                    5. 找到每个自由变量所绑定的父作用域:
                      1. console对于全局作用域是绑定
                      2. 李< / ol > < / > 函数引用在哪个作用域?# EYZ1。
                        1. 因此console不是关闭了f
                        2. 李< / ol > < / >

                        因此,函数f不是一个闭包。

                        # EYZ0:

                          列出变量:
                          1. console是一个免费的变量。
                          2. i2是一个免费的变量。
                          3. 李< / ol > < / >
                          4. 找到每个自由变量所绑定的父作用域:
                            1. console对于全局作用域是绑定
                            2. i2绑定f的范围。
                            3. 李< / ol > < / > 函数引用在哪个作用域?# EYZ2。
                              1. 因此console不是关闭了g
                              2. 因此i2 = 关闭了 * g
                              3. 李< / ol > < / >

                              因此,函数g是自由变量i2(它是g的一个向上值),它是setTimeout中的引用

                              你正在使用一个闭包。内部函数是一个闭包。

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

                              一个简单的解释为什么所有的函数都是闭包(credit @Peter):

                              首先让我们考虑下面的程序(它是控制):

                              lexicalScope();
                              
                              
                              function lexicalScope() {
                              var message = "This is the control. You should be able to see this message being alerted.";
                              
                              
                              regularFunction();
                              
                              
                              function regularFunction() {
                              alert(eval("message"));
                              }
                              }

                              1. 我们知道lexicalScoperegularFunction不是闭包从上面的定义
                              2. 当我们执行程序我们预计 message被警告因为 regularFunction不是一个闭包(即它可以访问父作用域中的所有变量-包括message)。
                              3. 当我们执行程序我们观察时,message确实被警告了。

                              接下来让我们考虑以下程序(它是替代):

                              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"));
                              };
                              }

                              1. 我们知道只有closureFunction是一个闭包从上面的定义
                              2. 当我们执行程序我们预计 message不被警告因为 closureFunction是一个闭包(即它只能访问函数创建的时间 (请看这个答案)的所有非本地变量 -这不包括message)。
                              3. 当我们执行程序我们观察时,message实际上正在被警告。

                              我们能从中推断出什么?

                              1. JavaScript解释器对待闭包的方式与对待其他函数的方式没有区别。
                              2. 每个函数都带有它的作用域链。闭包没有单独的引用环境。
                              3. 闭包就像其他函数一样。当它们在作用域中为引用,它们所属的作用域,因为时,我们称它们为闭包,这是一个有趣的例子。

我对任何人解释这件事的方式都不满意。

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

如果没有闭包,这将抛出一个错误

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


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

一旦outerFunc在一个假想的禁用闭包的JavaScript版本中返回,对outerVar的引用将被垃圾收集并消失,没有留下任何东西供内部func引用。

闭包本质上是一种特殊的规则,当内部函数引用外部函数的变量时,闭包可以使这些变量存在。使用闭包,即使在外部函数完成或“关闭”之后,引用的vars也会被维护,如果这有助于您记住要点的话。

即使使用闭包,在没有引用局部函数的内部函数的函数中,局部变量的生命周期也与在无闭包版本中工作相同。当活动结束时,当地人会收集垃圾。

一旦你在一个内部的func中引用了一个外部的变量,然而,它就像一个门框被放置在垃圾收集的方式为那些引用的变量。

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

重复触发一个不断递增的内部函数,并记录外部函数的局部变量,将不断警告更高的值。

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

我之前写这篇文章是为了提醒自己闭包是什么以及它在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'

这将创建并重新创建一个函数f,该函数在i上关闭,但它们是不同的!:

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'));

,而下面关闭“a”函数“本身”
(自己!之后的代码段使用了一个单独的引用f)

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

或者更明确地说:

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

NB。f的最后一个定义是function(){ console.log(9) } 之前 0

警告!闭包的概念可以强制地偏离初级编程的本质:

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

x-refs.:
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

我想分享我的例子和关于闭包的解释。我做了一个python示例和两个图来演示堆栈状态。

def maker(a, b, n):
margin_top = 2
padding = 4
def message(msg):
print('\n’ * margin_top, a * n,
' ‘ * padding, msg, ' ‘ * padding, b * n)
return message


f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')

这段代码的输出如下:

*****      hello      #####


      good bye!    ♥♥♥

下面两张图显示了堆栈和附加到函数对象的闭包。

当函数从maker返回

当函数稍后被调用

当通过参数或非局部变量调用函数时,代码需要局部变量绑定,如margin_top, padding以及a, b, n。为了确保函数代码正常工作,应该可以访问很久以前消失的maker函数的堆栈帧,它在我们可以找到的闭包中与函数消息对象一起备份。