576

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

让我们举这个例子。我们有一个计数循环,并希望在控制台上延迟打印计数器变量。因此,我们使用setTimeout闭包来捕获计数器变量的值,以确保它不会打印 N 次值 N。

没有闭包或任何接近包的错误解决方案是:

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

这当然会打印i循环后值的 10 倍,即 10。

所以他的尝试是:

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

按预期打印 0 到 9。

我告诉他他没有使用闭包来捕获i,但他坚持认为他是。我通过将 for 循环体放入另一个循环体(将他的匿名函数传递给)来证明他不使用闭包,再次打印 10 次 10 次。如果我将他的函数存储在 a 中并在循环之后执行它,同样适用,也打印 10 乘以 10。所以我的论点是他并没有真正捕获的值,使他的版本不是闭包。setTimeoutsetTimeoutvari

我的尝试是:

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

所以我捕获(在闭包中i命名),但现在我返回另一个函数并传递它。就我而言,传递给 setTimeout 的函数确实捕获了.i2i

现在谁在使用闭包,谁不使用?

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

4

12 回答 12

669

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

闭包的简单解释:

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

现在让我们用它来确定谁使用闭包,谁不使用(为了解释起见,我命名了函数):

案例一:你朋友的节目

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

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

对于f

  1. 列出变量:
    1. i2是一个局部变量。
    2. i是一个自由变量。
    3. setTimeout是一个自由变量。
    4. g是一个局部变量。
    5. console是一个自由变量。
  2. 查找每个自由变量绑定到的父作用域:
    1. i绑定到全局范围。
    2. setTimeout绑定到全局范围。
    3. console绑定到全局范围。
  3. 函数引用在哪个范围内?全局范围 。
    1. 因此i不被关闭f
    2. 因此setTimeout不被关闭f
    3. 因此console不被关闭f

因此该函数f不是闭包。

对于g

  1. 列出变量:
    1. console是一个自由变量。
    2. i2是一个自由变量。
  2. 查找每个自由变量绑定到的父作用域:
    1. console绑定到全局范围。
    2. i2绑定在的范围内f
  3. 函数引用在哪个范围内?的范围setTimeout
    1. 因此console不被关闭g
    2. 因此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);
}

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

对于f

  1. 列出变量:
    1. i2是一个局部变量。
    2. g是一个局部变量。
    3. console是一个自由变量。
  2. 查找每个自由变量绑定到的父作用域:
    1. console绑定到全局范围。
  3. 函数引用在哪个范围内?全局范围 。
    1. 因此console不被关闭f

因此该函数f不是闭包。

对于g

  1. 列出变量:
    1. console是一个自由变量。
    2. i2是一个自由变量。
  2. 查找每个自由变量绑定到的父作用域:
    1. console绑定到全局范围。
    2. i2绑定在的范围内f
  3. 函数引用在哪个范围内?的范围setTimeout
    1. 因此console不被关闭g
    2. 因此i2关闭g。_

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

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

所以你和你的朋友都在使用闭包。别吵了。我希望我清除了闭包的概念以及如何为你们俩识别它们。

编辑:关于为什么所有函数都关闭的简单解释(学分@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. 从上面的定义lexicalScope中,我们知道两者regularFunction都不是闭包。
  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. 从上面的定义我们知道 onlyclosureFunction是一个闭包。
  2. 当我们执行程序时,我们希望 message不会收到警报,因为 它是一个闭包(即它只能在创建函数时closureFunction访问其所有非局部变量参见这个答案) - 这不包括)。message
  3. 当我们执行程序时,我们观察message它实际上是被警告的。

我们从中推断出什么?

  1. JavaScript 解释器对待闭包的方式与对待其他函数的方式没有区别。
  2. 每个函数都带有它的作用域链。闭包没有单独的引用环境。
  3. 闭包就像所有其他函数一样。当它们在它们所属范围之外的范围内被引用时,我们只称它们为闭包,因为这是一个有趣的案例。
于 2012-10-17T10:01:32.337 回答
98

根据closure定义:

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

closure如果您定义一个使用在函数外部定义的变量的函数,则您正在使用。(我们称变量为自由变量)。
他们都使用closure(即使在第一个例子中)。

于 2012-10-17T08:59:40.700 回答
55

简而言之, 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。如前所述,我们现在有 2 个作用域;范围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,我们尝试访问foo在作用域中声明的变量f。但如前所述,我们可以访问在词法父函数中声明的任何变量,这里就是这种情况;g是 的词法父级f。因此hello被打印。
现在让我们看看这条线console.log(bar);。此时我们处于作用域中f,我们尝试访问bar在作用域中声明的变量gbar未在当前范围内声明且函数g不是 的父级f,因此bar未定义

实际上,我们也可以访问在词法“祖父”函数范围内声明的变量。因此,如果函数h中定义了一个函数g

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

thenh将能够访问在 function 、 和 . 范围内声明的h所有g变量f。这是通过闭包完成的。在 JavaScript 中,闭包允许我们访问在词法父函数、词法祖父函数、词法祖父函数等中声明的任何变量。这可以看作是作用域链 scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... 直到最后一个没有词法父函数的父函数。

窗口对象

实际上,链并没有在最后一个父函数处停止。还有一个特殊的范围;全局范围。每个未在函数中声明的变量都被认为是在全局范围内声明的。全球范围有两个专业;

  • 在全局范围内声明的每个变量都可以在任何地方访问
  • 在全局范围内声明的变量对应于window对象的属性。

foo因此,在全局范围内声明变量有两种方式;通过不在函数中声明它或通过设置foo窗口对象的属性。

两次尝试都使用了闭包

现在您已经阅读了更详细的说明,现在可能很明显两种解决方案都使用了闭包。但可以肯定的是,让我们做一个证明。

让我们创建一种新的编程语言;JavaScript-无闭包。顾名思义,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)
  })();
}

因此这将undefined在 JavaScript-No-Closure 中打印 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);
}

因此这将undefined在 JavaScript-No-Closure 中打印 10 次。

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

编辑:假设这 3 个代码片段未在全局范围内定义。否则,变量fooi将绑定到window对象,因此可以通过windowJavaScript 和 JavaScript-No-Closure 中的对象访问。

于 2012-10-17T09:33:55.437 回答
22

我从来没有对任何人解释这一点的方式感到满意。

理解闭包的关键是理解没有闭包的 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 中引用了外部 var,这就像门框被放置在那些引用的 var 的垃圾收集方式中。

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

但引用的上下文实际上是持久的,不像快照。反复触发返回的内部函数,该函数不断递增并记录外部函数的本地 var 将不断提醒更高的值。

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
于 2012-10-18T01:51:44.840 回答
17

你们都在使用闭包。

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

在计算机科学中,闭包(也称为词法闭包或函数闭包)是函数或对函数的引用以及引用环境——存储对该函数的每个非局部变量(也称为自由变量)的引用的表. 闭包——与普通函数指针不同——允许函数访问那些非局部变量,即使在其直接词法范围之外调用也是如此。

您朋友的尝试i通过获取其值并制作副本以存储到 local 中,显然使用了非本地变量i2

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

于 2012-10-17T09:08:52.787 回答
13

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

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

MDN:https ://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

在您朋友的代码函数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);
}
于 2012-10-17T09:03:01.227 回答
10

让我们看看两种方式:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

setTimeout()声明并立即执行在其自己的上下文中运行的匿名函数。的当前值i通过复制到i2first 来保存;它的工作原理是立即执行。

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

为内部函数声明一个执行上下文,从而将 的当前值i保存到i2; 这种方法还使用立即执行来保留价值。

重要的

应该提到的是,两种方法之间的运行语义并不相同;你的内部函数被传递给,setTimeout()而他的内部函数调用setTimeout()自己。

将两个代码包装在另一个代码中setTimeout()并不能证明只有第二种方法使用了闭包,只是一开始就不一样。

结论

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

于 2012-10-17T09:20:17.847 回答
10

关闭

闭包不是函数,也不是表达式。它必须被看作是函数范围外使用的变量和函数内部使用的一种“快照”。从语法上讲,应该说:'采取变量的闭包'。

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

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

所有人都在使用闭包。不要将执行点与闭包混淆。如果在错误的时刻拍摄了闭包的“快照”,则这些值可能是出乎意料的,但肯定会采取闭包!

于 2012-10-17T09:49:27.417 回答
9

我前段时间写了这篇文章,以提醒自己闭包是什么以及它在 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' 
于 2012-12-21T19:10:29.793 回答
6

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

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

在您的情况下,您正在访问存在的i2匿名函数内部console.log。在“范围变量”下的 chrome 开发人员工具之前和中添加一条debugger;语句console.log,它将告诉变量在什么范围内。

于 2012-10-17T08:58:44.597 回答
4

考虑以下。这将创建并重新创建一个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 );
}

注意。fis function(){ console.log(9) } before 的最后一个定义0被打印出来。

警告!闭包概念可能会强制分散基本编程的本质:

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

x-refs.:
JavaScript 闭包是如何工作的?
Javascript 闭包解释
(JS) 闭包是否需要函数内部的函数
如何理解 Javascript 中的闭包?
Javascript局部和全局变量混淆

于 2015-03-15T13:56:13.053 回答
-1

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

于 2018-05-12T03:27:56.863 回答