关于范围:JavaScript闭包与匿名函数

JavaScript closures vs. anonymous functions

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

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

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

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

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

所以他的尝试是:

1
2
3
4
5
6
7
8
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

按预期打印0到9。

我告诉他,他并没有利用一个关闭来抓捕埃多克斯(1),但他坚持说他是。我通过将for循环体放入另一个setTimeout中(将匿名函数传递给setTimeout)证明了他不使用闭包,再次打印10次10。如果我将他的函数存储在一个var中,并在循环后执行它,同样适用,也可以打印10次10。所以我的观点是,他并没有真正抓住i的价值,使得他的版本不是一个终结。

我的尝试是:

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

所以我在闭包中捕获了i(名为i2),但现在我返回另一个函数并传递这个函数。在我的例子中,传递给settimeout的函数确实捕获了i

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

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


编者按:正如本文所解释的,javascript中的所有函数都是闭包。然而,我们只关心从理论的角度识别这些函数的一个子集。此后,除非另有说明,否则对闭包一词的任何引用都将引用该功能子集。好的。

闭包的简单解释:好的。

  • 接受一个功能。我们称之为F。
  • 列出f的所有变量。
  • 变量可以有两种类型:
  • 局部变量(绑定变量)
  • 非局部变量(自由变量)
  • 如果f没有自由变量,那么它就不能是一个闭包。
  • 如果f有任何自由变量(在f的父范围中定义),则:
  • 自由变量绑定到的f的父作用域必须只有一个。
  • 如果从父范围之外引用f,则它将成为该自由变量的闭包。
  • 这个自由变量被称为闭包f的upvalue。
  • 现在让我们用这个来确定谁使用闭包,谁不使用闭包(为了解释,我已经命名了函数):好的。

    案例1:你朋友的计划好的。

    1
    2
    3
    4
    5
    6
    7
    8
    for (var i = 0; i < 10; i++) {
        (function f() {
            var i2 = i;
            setTimeout(function g() {
                console.log(i2);
            }, 1000);
        })();
    }

    在上述程序中有两个功能:fg。让我们看看它们是否是闭包:好的。

    对于f:好的。

  • 列出变量:
  • i2是一个局部变量。
  • i是一个自由变量。
  • setTimeout是一个自由变量。
  • g是一个局部变量。
  • console是一个自由变量。
  • 查找每个自由变量绑定到的父作用域:
  • i受全球范围的约束。
  • setTimeout受全球范围的约束。
  • console受全球范围的约束。
  • 函数引用的范围是什么?全球范围。
  • 因此,i不是由f关闭的。
  • 因此,setTimeout不是由f关闭的。
  • 因此,console不是由f关闭的。
  • 因此,函数f不是一个闭包。好的。

    对于g:好的。

  • 列出变量:
  • console是一个自由变量。
  • i2是一个自由变量。
  • 查找每个自由变量绑定到的父作用域:
  • console受全球范围的约束。
  • i2属于f的范围。
  • 函数引用的范围是什么?setTimeout的范围。
  • 因此,console不是由g关闭的。
  • 因此,i2g关闭。
  • 因此,当从setTimeout中引用时,函数g是自由变量i2的闭包(它是g的一个upvalue)。好的。

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

    案例2:你的计划好的。

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i < 10; i++) {
        setTimeout((function f(i2) {
            return function g() {
                console.log(i2);
            };
        })(i), 1000);
    }

    在上述程序中有两个功能:fg。让我们看看它们是否是闭包:好的。

    对于f:好的。

  • List the variables:
  • 字母名称0是地方变量。
  • 字母名称是地方变量。
  • EDOCX1是一个自由变量。
  • Find the parent scope to which each free variable is bound:
  • 爱多克X1是通往全球范围的通道
  • 在什么范围是功能参考?The Global Scope.
  • Hence EDOCX1(英文)2[英文]5[英文]5[英文]
  • (P)如果这个函数是EDOCX1,那么它不是一个关闭。好的,好的。(P)致EDOCX1:好的,好的。

  • List the variables:
  • EDOCX1是一个自由变量。
  • 字母名称0是一个自由变量。
  • Find the parent scope to which each free variable is bound:
  • 爱多克X1是通往全球范围的通道
  • EDOCX1 0是一个充满活力的音乐剧5
  • 在什么范围是功能参考?EDOCX1的音标13
  • Hence EDOCX1(英文)2[英文]2[英文]
  • 伊多克x1的英文字母是用伊多克x1封杀的。
  • (P)Thus the function EDOCX1 pental 1 is a closure for the free variable EDOCX1 universal 0(which is an upvalue for EDOCX1 penal 1),when it's referenced from within EDOCX1 penal 13.好的,好的。(P)Good for you:You are using a closure.功能本身就是一种封闭。好的,好的。(P)So both you and your friend are using closure.停止Arguing。I hope I cleared the concept of closure and how to identify them for the both of you.好的,好的。(P)Edit:a simple explanation as to why are all functions closure(credits@Peter):好的,好的。(P)3.First let's consider the following program(it's the control):好的,好的。(P)字母名称好的,好的。

  • We know that both EDOCX1 plographic 22 welcx1 and EDOCX1 commercial 23 nable Aren't closures from the above definition.
  • When we execute the program we expect EDOCX1 single 24 to be alerted because EDOCX1 pental 23 communal is not a closure(i.e.it has access to all the variables in its parent scope-including EDOCX1 penal 24).
  • When we execute the program we observe that EDOCX1 disciplinary 24 nable is indeed alert.
  • (P)Next let's consider the following program(it's the alternative):好的,好的。(P)字母名称好的,好的。

  • We know that only EDOCX1 pental 28 is a closure from the above definition.
  • When we execute the program we expect EDOCX1 plography 24 sinki not to be alerted because EDOCX1 pental 28)is a closure(i.e.it only has access to all its non-local variables at the time the function is created(see this answer)-this does not include EDOCX1.
  • When we execute the program we observe that EDOCX1 single 24 is actually being alert.
  • (P)我们到底是怎么了?好的,好的。

  • Javascript interpreters do not treat closure differently from the way they treat other functions.
  • 每个函数都有自己的范围Chain along with it.Closures don't have a separate referenceing environment.
  • a closure is just like every other function.We just call them closure when they are referenced in a scope outside the scope to which they belong because this is an interesting case.
  • 好吧。


    根据closure的定义:

    A"closure" is an expression (typically a function) that can have free variables together with an environment that binds those variables (that"closes" the expression).

    如果定义的函数使用的变量是在函数外部定义的,则使用closure。(我们称之为自由变量)。它们都使用closure(甚至在第一个示例中也是如此)。


    简而言之,JavaScript闭包允许函数访问在词法父函数中声明的变量。好的。

    让我们看一个更详细的解释。要理解闭包,重要的是要了解JavaScript如何定义变量。好的。

    作用域好的。

    在javascript中,作用域是用函数定义的。每个函数都定义了一个新的作用域。好的。

    考虑以下示例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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打印好的。

    1
    2
    3
    4
    hello
    hello
    2
    Am I Accessible?

    现在我们来考虑一下,我们在另一个函数f中定义了一个函数g。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    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作用域中声明的任何变量。好的。

    考虑以下事项:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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打印好的。

    1
    2
    hello
    undefined

    让我们看一看console.log(foo);行。此时,我们在g范围内,我们试图访问在f范围内声明的变量foo。但如前所述,我们可以访问词汇父函数中声明的任何变量,这里就是这种情况;gf的词汇父函数。因此打印了hello。现在我们来看看console.log(bar);行。此时,我们在f范围内,我们试图访问在g范围内声明的变量barbar未在当前范围内声明,函数g不是f的父级,因此bar未定义。好的。

    实际上,我们还可以访问词汇"grand parent"函数范围内声明的变量。因此,如果在函数g中定义了一个函数h。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    那么,h就可以访问函数hgf范围内声明的所有变量。这是用闭包完成的。在javascript闭包中,我们可以访问在词法父函数、词法父函数、词法父函数等中声明的任何变量。这可以看作是一个作用域链; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... ,直到最后一个没有词法父级的父函数为止。好的。

    窗口对象好的。

    实际上,链不会在最后一个父函数处停止。还有一个更特殊的范围:全局范围。未在函数中声明的每个变量都被认为是在全局范围中声明的。全球范围有两个特点;好的。

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

    因此,在全局范围内声明变量foo有两种方法:要么不在函数中声明,要么设置窗口对象的属性foo。好的。

    两次尝试都使用闭包好的。

    既然您已经阅读了更详细的解释,那么现在很明显这两个解决方案都使用闭包。但要确定的是,让我们做一个证明。好的。

    让我们创建一种新的编程语言;javascript没有闭包。顾名思义,javascript没有一个闭包与javascript相同,只是它不支持闭包。好的。

    换言之;好的。

    1
    2
    3
    4
    5
    var foo = 'hello';
    function f(){console.log(foo)};
    f();
    //JavaScript-No-Closure prints undefined
    //JavaSript prints hello

    好吧,让我们看看第一个没有闭包的javascript解决方案会发生什么情况;好的。

    1
    2
    3
    4
    5
    6
    7
    8
    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而不关闭。好的。

    因此,第一个解决方案使用闭包。好的。

    让我们看看第二个解决方案;好的。

    1
    2
    3
    4
    5
    6
    7
    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而不关闭。好的。

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

    编辑:假设这3个代码段没有在全局范围内定义。否则,变量fooi将绑定到window对象,因此可以通过javascript和javascript中的window对象访问,而不会关闭。好的。好啊。


    我从来没有对任何人解释这件事的方式感到高兴。

    理解闭包的关键是理解没有闭包的JS是什么样子的。

    如果没有闭包,这将引发一个错误

    1
    2
    3
    4
    5
    6
    7
    8
    function outerFunc(){
        var outerVar = 'an outerFunc var';
        return function(){
            alert(outerVar);
        }
    }

    outerFunc()(); //returns inner function and fires it

    一旦outerfunc在一个虚构的禁用闭包的javascript版本中返回,对outervar的引用将被垃圾收集并消失,不会留下任何内容供内部func引用。

    闭包本质上是一种特殊的规则,当内部函数引用外部函数的变量时,这些变量就可能存在。使用闭包时,即使在完成外部函数之后,也会维护引用的var,如果这样做有助于记住要点,则也可以使用"closed"。

    即使使用闭包,在没有引用其局部函数的内部函数的函数中,局部变量的生命周期工作方式与在无闭包版本中相同。函数完成后,局部变量将被垃圾收集。

    一旦在内部func中有一个对外部var的引用,但是它就像一个门柱被放置在这些引用var的垃圾收集方式中。

    查看闭包的一个更准确的方法是,内部函数基本上使用内部范围作为自己的范围定义。

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

    1
    2
    3
    4
    5
    6
    7
    function outerFunc(){
        var incrementMe = 0;
        return function(){ incrementMe++; console.log(incrementMe); }
    }
    var inc = outerFunc();
    inc(); //logs 1
    inc(); //logs 2


    你们都在使用闭包。

    我要用维基百科的定义:

    In computer science, a closure (also lexical closure or function
    closure) is a function or reference to a function together with a
    referencing environment—a table storing a reference to each of the
    non-local variables (also called free variables) of that function.
    A closure—unlike a plain function pointer—allows a function to access
    those non-local variables even when invoked outside of its immediate
    lexical scope.

    您朋友的尝试显然使用了变量i,它是非本地的,通过利用它的值并复制到本地i2中来存储。

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


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

    A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created.

    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

    另一个等价物(但内存利用率较低)是:

    1
    2
    3
    4
    5
    6
    7
    8
    function fGenerator(i2){
        return function(){
            console.log(i2);
        }
    }
    for(var i = 0; i < 10; i++) {
        setTimeout(fGenerator(i), 1000);
    }


    关闭

    闭包不是函数,也不是表达式。它必须被视为一种"快照",来自函数作用域之外和函数内部使用的已用变量。从语法上讲,应该说:"取变量的闭包"。

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

    再一次(NA)?f):闭包可以访问不作为参数传递的变量。

    记住,这些功能概念很大程度上取决于您使用的编程语言/环境。在JavaScript中,闭包依赖于词汇范围(在大多数C语言中都是这样)。

    因此,返回一个函数主要是返回一个匿名/未命名的函数。当函数访问变量(不是作为参数传递)在其(词汇)范围内时,将执行一个闭包。

    因此,关于你的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 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)
    }

    他们都在使用闭包。不要将执行点与闭包混淆。如果在错误的时刻获取闭包的"快照",则值可能是意外的,但肯定会获取闭包!


    让我们看看这两种方法:

    1
    2
    3
    4
    5
    6
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();

    声明并立即执行一个匿名函数,该函数在自己的上下文中运行setTimeout()i的当前值是通过首先将副本复制到i2中来保留的;它是由于立即执行而起作用的。

    1
    2
    3
    4
    5
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);

    声明内部函数的执行上下文,其中i的当前值保留到i2中;此方法还使用立即执行来保留该值。

    重要的

    应该指出,两种方法之间的运行语义不相同;您的内部函数被传递给setTimeout(),而其内部函数调用setTimeout()

    将这两个代码包装在另一个setTimeout()中并不能证明只有第二种方法使用闭包,只是开始时没有相同的事情。

    结论

    这两种方法都使用闭包,因此归根结底,这取决于个人的喜好;第二种方法更容易"移动"或概括。


    我刚才写这篇文章是为了提醒自己闭包是什么以及它在JS中的工作方式。

    闭包是一个函数,当调用它时,它使用声明它的作用域,而不是调用它的作用域。在JavaScript中,所有函数的行为都是这样的。只要有一个函数仍然指向某个范围中的变量值,该范围中的变量值就会保持不变。规则的例外是"this",它指的是调用函数时该函数位于内部的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var z = 1;
    function x(){
        var z = 2;
        y(function(){
          alert(z);
        });
    }
    function y(f){
        var z = 3;
        f();
    }
    x(); //alerts '2'

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

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

    在您的例子中,您正在访问匿名函数内的i2,其中console.log存在。在console.log之前添加一条debugger;语句,在chrome开发者工具的"范围变量"下,它将告诉你变量在什么范围内。


    考虑以下内容。这将创建并重新创建一个函数f,该函数在i上关闭,但不同!:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    i=100;

    f=function(i){return function(){return ++i}}(0);
    alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('

    '
    ));

    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('

    '
    ));

    当以下内容关闭"a"函数本身时(他们自己!后面的代码段使用单个引用f

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

    或者更明确地说:

    1
    2
    3
    4
    for(var i = 0; i < 10; i++) {
        console.log(    f = new Function( 'console.log('+i+')' )    );
        setTimeout( f,  1000 );
    }

    铌。f的最后一个定义是function(){ console.log(9) }0打印之前。

    警告!闭包概念可能是强制分散对基本编程本质的注意力:

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

    X参考文献:JavaScript闭包是如何工作的?JavaScript闭包说明(JS)闭包是否需要函数内部的函数如何理解JavaScript中的闭包?javascript局部和全局变量混淆


    我想分享我的例子和一个关于闭包的解释。我做了一个Python示例,并用两个数字来演示堆栈状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def maker(a, b, n):
        margin_top = 2
        padding = 4
        def message(msg):
            print('
    ’ * margin_top, a * n,
                '
    * padding, msg, ' ‘ * padding, b * n)
        return message

    f = maker('
    *', '#', 5)
    g = maker('
    ?', '?, 3)

    f('hello')
    g(‘good bye!')

    此代码的输出如下:

    1
    2
    3
    *****      hello      #####

    ???      good bye!    ???

    这里有两个图显示堆栈和附加到函数对象的闭包。

    当函数从maker返回时

    稍后调用函数时

    当通过参数或非局部变量调用函数时,代码需要局部变量绑定,如Margin_Top、Padding以及a、b、n。为了确保函数代码正常工作,很久以前就不存在的maker函数的堆栈框架应该是可访问的,它备份在闭包中,我们可以通过h函数消息对象。