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


当前回答

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

闭包是一种特殊的对象,它结合了两个东西:函数和创建该函数的环境。环境由创建闭包时范围内的任何局部变量组成。 MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

在你朋友的代码函数function(){console.log(i2);}定义在匿名函数闭包内的函数function(){var i2 = i;…可读写局部变量i2。

在代码中,函数function(){console.log(i2);}定义在函数闭包内的函数函数(i2){返回…并且可以读/写本地值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);
}

其他回答

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

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

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

你们都在使用闭包。

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

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

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

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

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

结论

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

编者注:如本文所述,JavaScript中的所有函数都是闭包。然而,我们只对从理论角度来看有趣的这些函数的子集感兴趣。从今以后,除非另有说明,任何对闭包一词的引用都是指这个函数子集。

闭包的一个简单解释:

Take a function. Let's call it F. List all the variables of F. The variables may be of two types: Local variables (bound variables) Non-local variables (free variables) If F has no free variables then it cannot be a closure. If F has any free variables (which are defined in a parent scope of F) then: There must be only one parent scope of F to which a free variable is bound. If F is referenced from outside that parent scope, then it becomes a closure for that free variable. That free variable is called an upvalue of the closure F.

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

案例1:你朋友的程序

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

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

f:

List the variables: i2 is a local variable. i is a free variable. setTimeout is a free variable. g is a local variable. console is a free variable. Find the parent scope to which each free variable is bound: i is bound to the global scope. setTimeout is bound to the global scope. console is bound to the global scope. In which scope is the function referenced? The global scope. Hence i is not closed over by f. Hence setTimeout is not closed over by f. Hence console is not closed over by f.

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

g:

列出变量: 控制台是一个自由变量。 I2是自由变量。 找到每个自由变量所绑定的父作用域: 控制台绑定到全局作用域。 I2限定在f的作用域内。 函数在哪个范围内被引用?setTimeout的范围。 因此控制台不会被g关闭。 因此i2除以g。

因此,函数g是自由变量i2(它是g的上值)的闭包,当它从setTimeout中引用时。

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

案例2:您的程序

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

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

f:

列出变量: I2是一个局部变量。 G是一个局部变量。 控制台是一个自由变量。 找到每个自由变量所绑定的父作用域: 控制台绑定到全局作用域。 函数在哪个范围内被引用?全球范围。 因此控制台不会被f关闭。

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

g:

列出变量: 控制台是一个自由变量。 I2是自由变量。 找到每个自由变量所绑定的父作用域: 控制台绑定到全局作用域。 I2限定在f的作用域内。 函数在哪个范围内被引用?setTimeout的范围。 因此控制台不会被g关闭。 因此i2除以g。

因此,函数g是自由变量i2(它是g的上值)的闭包,当它从setTimeout中引用时。

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

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

编辑:关于为什么所有函数都是闭包的简单解释(credit @Peter):

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

lexicalScope (); 函数lexicalScope() { var message = "这是控件。你应该能看到这条消息被提醒了。” regularFunction (); 函数正则函数(){ alert (eval(“信息”)); } }

我们知道lexicalScope和regularFunction都不是上述定义中的闭包。 当我们执行程序时,我们期望消息被警告,因为regularFunction不是一个闭包(即它可以访问父作用域中的所有变量——包括消息)。 当我们执行程序时,我们观察到消息确实被警告了。

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

var closureFunction = lexicalScope(); closureFunction (); 函数lexicalScope() { var message = "这是另一种选择。如果你看到这个消息被警告,那么就意味着JavaScript中的每个函数都是一个闭包。” 返回函数closureFunction() { alert (eval(“信息”)); }; }

从上面的定义中,我们知道只有closureFunction是闭包。 当我们执行程序时,我们希望message不会被警告,因为closureFunction是一个闭包(也就是说,在函数创建时,它只能访问它的所有非局部变量(见这个答案)-这不包括message)。 当我们执行程序时,我们观察到消息实际上正在被警告。

我们能从中推断出什么?

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