我的一个朋友和我正在讨论什么是JS中的闭包,什么不是。我们只是想确保我们理解正确。

让我们举个例子。我们有一个计数循环,并希望在控制台上延迟打印计数器变量。因此,我们使用setTimeout和闭包来捕获计数器变量的值,以确保它不会打印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),但现在我返回另一个函数并传递它。在我的例子中,传递给setTimeout的函数实际上捕获了i。

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

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


当前回答

简而言之,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?

现在考虑这样的情况,函数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。

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

闭包

在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

Let's look at the line console.log(foo);. At this point we are in scope g and we try to access the variable foo that is declared in scope f. But as stated before we can access any variable declared in a lexical parent function which is the case here; g is the lexical parent of f. Therefore hello is printed. Let's now look at the line console.log(bar);. At this point we are in scope f and we try to access the variable bar that is declared in scope g. bar is not declared in the current scope and the function g is not the parent of f, therefore bar is undefined

实际上,我们还可以访问词法“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

then h would be able to access all the variables declared in the scope of function h, g, and f. This is done with closures. In JavaScript closures allows us to access any variable declared in the lexical parent function, in the lexical grand parent function, in the lexical grand-grand parent function, etc. This can be seen as a scope chain; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... until the last parent function that has no lexical parent.

窗口对象

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

在全局作用域中声明的每个变量都可以在任何地方访问 在全局作用域中声明的变量对应于窗口对象的属性。

因此,在全局作用域中声明变量foo有两种方法;要么不在函数中声明它,要么设置window对象的属性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中打印10次undefined - 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);
}

因此,这将在javascript中打印10次undefined - no - closure。

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

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

其他回答

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

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

在您的情况下,您正在访问匿名函数中的i2,其中console.log存在。添加调试器;在console.log和chrome开发工具的“作用域变量”下,它会告诉变量的作用域。

简而言之,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?

现在考虑这样的情况,函数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。

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

闭包

在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

Let's look at the line console.log(foo);. At this point we are in scope g and we try to access the variable foo that is declared in scope f. But as stated before we can access any variable declared in a lexical parent function which is the case here; g is the lexical parent of f. Therefore hello is printed. Let's now look at the line console.log(bar);. At this point we are in scope f and we try to access the variable bar that is declared in scope g. bar is not declared in the current scope and the function g is not the parent of f, therefore bar is undefined

实际上,我们还可以访问词法“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

then h would be able to access all the variables declared in the scope of function h, g, and f. This is done with closures. In JavaScript closures allows us to access any variable declared in the lexical parent function, in the lexical grand parent function, in the lexical grand-grand parent function, etc. This can be seen as a scope chain; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... until the last parent function that has no lexical parent.

窗口对象

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

在全局作用域中声明的每个变量都可以在任何地方访问 在全局作用域中声明的变量对应于窗口对象的属性。

因此,在全局作用域中声明变量foo有两种方法;要么不在函数中声明它,要么设置window对象的属性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中打印10次undefined - 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);
}

因此,这将在javascript中打印10次undefined - no - closure。

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

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

让我们看看这两种方式:

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

结论

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

我想分享我的例子和关于闭包的解释。我做了一个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函数的堆栈帧,它在我们可以找到的闭包中与函数消息对象一起备份。

关闭

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

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

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

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