首页 文章

JavaScript闭包与匿名函数

提问于
浏览
532

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

我们来看看这个例子吧 . 我们有一个计数循环,并希望在控制台上打印计数器变量延迟 . 因此,我们使用 setTimeoutclosures 来捕获计数器变量的值,以确保它不会打印N倍N值 .

没有 closures 或接近 closures 的错误解决方案将是:

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

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

So his attempt was:

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

按预期打印0到9 .

我告诉他,他并没有使用 closure 捕获 i ,但他坚持认为他是 . 我证明他没有使用 closures 将for循环体放在另一个 setTimeout (将他的匿名函数传递给 setTimeout ),再次打印10次10 . 如果我将他的函数存储在 var 并在循环之后执行它,同样打印10次10,这同样适用 . 所以我的论点是 he doesn't really capture the value of i ,使他的版本不是一个闭包 .

My attempt was:

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

所以我捕获 i (在闭包中命名为 i2 ),但现在我返回另一个函数并传递它 . In my case, the function passed to setTimeout really captures i.

Now who is using closures and who isn't?

请注意,两个解决方案在控制台上打印0到9都会延迟,因此它们解决了原始问题,但我们想要了解这两个解决方案中的哪一个 uses closures 来实现此目的 .

12 回答

  • 47

    我想分享我的例子和关于闭包的解释 . 我做了一个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!    ♥♥♥
    

    下面是两个显示堆栈和附加到函数对象的闭包的图 .

    when the function is returned from maker

    when the function is called later

    当通过参数或非局部变量调用函数时,代码需要局部变量绑定,例如margin_top,padding以及a,b,n . 为了确保函数代码能够工作,很久以前就已经消失的制造商函数的堆栈框架应该是可访问的,这在我们可以找到的函数消息对象的闭包中备份 .

  • 624

    简而言之 Javascript Closures 允许函数 access a variable ,即 declared in a lexical-parent function .

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

    Scopes

    在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?
    

    现在让我们考虑一下我们在另一个函数 f 中定义函数 g 的情况 .

    function f()
    {//begin of scope f
      function g()
      {//being of scope g
        /*...*/
      }//end of scope g
      /*...*/
    }//end of scope f
    

    我们将 lexical parentlexical parent 称为 lexical parent . 如前所述,我们现在有两个范围;范围 f 和范围 g .

    但是一个范围是“在”另一个范围内,那么子函数范围是父函数范围的一部分吗?在父函数范围内声明的变量会发生什么?我能从子功能的范围访问它们吗?这正是关闭步骤的地方 .

    Closures

    在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 中,我们尝试访问在范围 f 中声明的变量 foo . 但是如前所述,我们可以访问词法父函数中声明的任何变量,这是这里的情况; gf 的词汇父级 . 因此打印 hello .
    我们现在看看 console.log(bar); 行 . 此时,我们在范围 f 中,并且我们尝试访问在范围 g 中声明的变量 bar . bar 未在当前范围内声明,函数 g 不是 f 的父级,因此 bar 未定义

    实际上我们也可以访问在词法"grand parent"函数范围内声明的变量 . 因此,如果在函数 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
    

    然后 h 将能够访问在函数 hgf 范围内声明的所有变量 . 这是通过 closures 完成的 . 在JavaScript中 closures 允许我们访问词法父函数,词法祖父函数,词汇祖父函数等中声明的任何变量 . 这可以看作 scope chain ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... 直到最后一个没有词法父项的父函数 .

    The window object

    实际上链不会停在最后一个父函数 . 还有一个特殊的范围; global scope . 未在函数中声明的每个变量都被视为在全局范围内声明 . 全球范围有两个专业;

    • 声明的每个变量全球范围可以访问 everywhere

    • 在全局范围内声明的变量对应于 window 对象的属性 .

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

    Both attempts uses closures

    现在你已经阅读了更详细的解释,现在很明显两个解决方案都使用了闭包 . 但可以肯定的是,让我们来证明一下 .

    让我们创建一种新的编程语言; 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)
      })();
    }
    

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

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

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

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

  • 3

    Editor's Note: JavaScript中的所有函数都是闭包,如post中所述 . 然而,我们只想从理论的角度来识别这些函数的子集,这些函数是interesting . 此后,除非另有说明,否则对闭包一词的任何引用都将指代该功能子集 .

    闭包的简单解释:

    • 采取功能 . 我们称之为F.

    • 列出F的所有变量 .

    • 变量可以有两种类型:

    • 局部变量(绑定变量)

    • 非局部变量(自由变量)

    • 如果F没有自由变量,则它不能是闭包 .

    • 如果F有任何自由变量(在F的 a 父范围中定义),则:

    • 必须只有一个F的父作用域, a 自由变量才能绑定到该作用域 .

    • 如果F是来自 that 父范围之外的 referenced ,那么它将成为 that 自由变量的闭包 .

    • That 自由变量被称为闭包F的upvalue .

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

    Case 1: Your Friend's Program

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

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

    对于 f

    • 列出变量:

    • i2local 变量 .

    • ifree 变量 .

    • setTimeoutfree 变量 .

    • glocal 变量 .

    • consolefree 变量 .

    • 查找每个自由变量绑定到的父作用域:

    • ibound 到全球范围 .

    • setTimeoutbound 到全球范围 .

    • consolebound 到全球范围 .

    • 在哪个范围是函数 referencedglobal scope .

    • 因此 i 不是 closed over by f .

    • 因此 setTimeout 不是 closed over by f .

    • 因此 console 不是 closed over by f .

    因此函数 f 不是闭包 .

    对于 g

    • 列出变量:

    • consolefree 变量 .

    • i2free 变量 .

    • 查找每个自由变量绑定到的父作用域:

    • consolebound 到全球范围 .

    • i2boundf 的范围 .

    • 在哪个范围是函数 referencedscope of setTimeout .

    • 因此 console 不是 closed over by g .

    • 因此 i2closed over by g .

    因此函数 g 是自由变量 i2 的闭包(这是 g 的upvalue) when setTimeout 来自 setTimeout .

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

    Case 2: Your Program

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

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

    对于 f

    • 列出变量:

    • i2local 变量 .

    • glocal 变量 .

    • consolefree 变量 .

    • 查找每个自由变量绑定到的父作用域:

    • consolebound 到全球范围 .

    • 在哪个范围是函数 referencedglobal scope .

    • 因此 console 不是 closed over f .

    因此函数 f 不是闭包 .

    对于 g

    • 列出变量:

    • consolefree 变量 .

    • i2free 变量 .

    • 查找每个自由变量绑定到的父作用域:

    • consolebound 到全局范围 .

    • i2 是范围的 bound f

    • 在哪个范围是函数 referencedscope of setTimeout .

    • 因此 console 不是 closed over g .

    • 因此 i2closed over by g .

    因此函数 g 是一个自由变量 i2 的闭包(这是 g 的一个upvalue) when 它是 setTimeout setTimeout .

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

    所以你和你的朋友都在使用闭包 . 停止争论 . 我希望我清除了闭包的概念以及如何为你们两个识别它们 .

    Edit: 关于为什么所有函数都关闭的简单解释(信用@Peter):

    首先让's consider the following program (it' s为control):

    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"));
        }
    }
    
    • 我们知道 lexicalScoperegularFunction 都不是闭包 from the above definition .

    • 当我们执行程序 we expect message 被警告时 because regularFunction 不是一个闭包(即它有权访问其父范围内的变量 all - 包括 message ) .

    • 当我们执行程序 we observe 时, message 确实被警告了 .

    接下来让's consider the following program (it'是alternative):

    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"));
        };
    }
    
    • 我们知道只有 closureFunction 是一个闭包 from the above definition .

    • 当我们执行程序 we expect message 没有被提醒时 because closureFunction 是一个闭包(即它只能访问 the time the function is created 的所有 non-local variablessee this answer) - 这不包括 message ) .

    • 当我们执行程序 we observe 时, message 实际上正在被警告 .

    我们从中推断出什么?

    • JavaScript解释器不会将闭包与处理其他函数的方式区别对待 .

    • 每个函数都带有scope chain . 闭包没有 separate 引用环境 .

    • 闭包和其他所有函数一样 . 我们只是在 referenced 范围 outside 中调用它们闭包它们所属的范围 because 这是一个有趣的案例 .

  • 9

    你们都在使用闭包 .

    我要去Wikipedia definition这里:

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

    您朋友的尝试通过获取其值并将副本存储到本地 i2 ,明确使用非本地变量 i .

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

  • 0

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

    闭包是一种特殊的对象,它结合了两个东西:一个函数,以及创建该函数的环境 . 环境由创建闭包时在范围内的任何局部变量组成 . 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 ... 的闭包,并且可以读/写本地有 Value 的 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);
    }
    
  • 9

    Closure

    闭包不是函数,也不是表达式 . 它必须被视为函数范围外使用的变量的一种“快照”,并在函数内部使用 . 在语法上,人们应该说:“关闭变量” .

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

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

    所有人都在使用闭包 . 不要将执行点与闭包混淆 . 如果在错误的时刻采取闭包的“快照”,则值可能是意外的,但肯定会关闭!

  • 5

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

    理解闭包的关键是理解没有闭包的JS会是什么样的 .

    Without closures, this would throw an error

    function outerFunc(){
        var outerVar = 'an outerFunc var';
        return function(){
            alert(outerVar);
        }
    }
    
    outerFunc()(); //returns inner function and fires it
    

    一旦outerFunc以假想的关闭禁用版本的JavaScript返回,对outerVar的引用将被垃圾收集并且不再留下任何内容以供引用的内部函数 .

    闭包本质上是特殊规则,当内部函数引用外部函数的变量时,这些规则可以使这些变量存在 . 对于闭包,即使在外部函数完成后也会保持引用的变量,如果这有助于您记住该点,则将其“关闭” .

    即使使用闭包,在没有引用其本地的内部函数的函数中的局部变量的生命周期与无闭包版本中的相同 . 功能完成后,本地人会收集垃圾 .

    一旦你有了在内部函数中引用外部var,但它就像一个doorjamb被置于那些引用变量的垃圾收集方式中 .

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

    But the context referenced is in fact, persistent, not like a snapshot. 反复触发返回的内部函数,该函数继续递增并记录外部函数的局部变量将继续警告更高的值 .

    function outerFunc(){
        var incrementMe = 0;
        return function(){ incrementMe++; console.log(incrementMe); }
    }
    var inc = outerFunc();
    inc(); //logs 1
    inc(); //logs 2
    
  • 93

    仔细检查后,看起来你们两个都在使用关闭 .

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

    在您的情况下,您正在匿名函数中访问 i2 ,其中存在 console.log . 在 console.log 之前添加 debugger; 语句,在"Scope variables"下的chrome开发人员工具中添加 debugger; 语句,它将告知变量的范围 .

  • 19

    考虑以下 . 这将创建并重新创建一个在 i 上关闭但不同的函数 f !:

    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"函数"itself"
    (他们自己!之后使用单个指示物的片段 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 );
    }
    

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

    警告!封闭概念可能是对初级编程本质的强制分心:

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

    X-裁判:
    How do JavaScript closures work?
    Javascript Closures Explanation
    Does a (JS) Closure Require a Function Inside a Function
    How to understand closures in Javascript?
    Javascript local and global variable confusion

  • 16

    我刚才写了这篇文章,提醒自己一个闭包是什么以及它在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'
    
  • 12

    让我们看看两种方式:

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

    声明并立即执行在其自己的上下文中运行 setTimeout() 的匿名函数 . 通过首先复制到 i2 来保留 i 的当前值;它的作用是因为立即执行 .

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

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

    Important

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

    将这两个代码包装在另一个 setTimeout() 并不是一回事就开始了 .

    Conclusion

    这两种方法都使用闭合,因此它归结为个人品味;第二种方法更容易“移动”或概括 .

  • 7

    根据 closure 定义:

    “闭包”是一个表达式(通常是一个函数),它可以将自由变量与绑定这些变量的环境(“关闭”表达式)结合在一起 .

    如果定义了一个使用在函数外部定义的变量的函数,则使用 closure . (我们将变量称为 free variable ) .
    他们都使用 closure (即使在第一个例子中) .

相关问题