我的一个朋友和我正在讨论什么是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,因此它们解决了最初的问题,但我们想了解这两个解决方案中哪一个使用闭包来完成这一任务。


当前回答

根据闭包定义:

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

如果你定义的函数使用了一个在函数外部定义的变量,那么你就是在使用闭包。(我们称这个变量为自由变量)。 它们都使用闭包(甚至在第一个例子中)。

其他回答

让我们看看这两种方式:

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

结论

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

关闭

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

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

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

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

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

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

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

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

理解闭包的关键是理解没有闭包的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

简而言之,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中都可以通过窗口对象访问。