我的一个朋友和我正在讨论什么是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);
}

其他回答

让我们看看这两种方式:

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

结论

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

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

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

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

闭包是一种特殊的对象,它结合了两个东西:函数和创建该函数的环境。环境由创建闭包时范围内的任何局部变量组成。 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);
}

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

编者注:如本文所述,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解释器对待闭包的方式与对待其他函数的方式没有区别。 每个函数都带有它的作用域链。闭包没有单独的引用环境。 闭包就像其他函数一样。当它们在它们所属的作用域之外的作用域中被引用时,我们只称它们为闭包,因为这是一个有趣的情况。