JavaScript闭包与匿名函数

lee*_*mes 551 javascript closures scope

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

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

错误的解决方案,无需关闭或接近任何倒闭将是:

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

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

所以他的尝试是:

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

按预期打印0到9.

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

我的尝试是:

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

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

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

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

Aad*_*hah 643

编者按:在JavaScript中所有的功能都关闭在这个解释.不过我们只是在识别这些功能,这是一个子集,有兴趣的有趣从理论角度.此后,除非另有说明,否则对闭包一词的任何引用都将指代该功能子集.

闭包的简单解释:

  1. 拿一个功能.我们称之为F.
  2. 列出F的所有变量
  3. 变量可以有两种类型:
    1. 局部变量(绑定变量)
    2. 非局部变量(自由变量)
  4. 如果F没有自由变量那么它就不能成为闭包.
  5. 如果F有任何自由变量(在F 父范围中定义),则:
    1. 必须有其中的F只有一个父范围一个自由变量绑定.
    2. 如果F被引用从外部父范围,则它成为一个封闭自由变量.
    3. 自由变量称为闭包F的upvalue.

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

案例1:您朋友的计划

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

在上面的程序中有两个功能: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(它是一个upvalue g)的闭包.setTimeout

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

案例2:您的计划

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

在上面的程序中有两个功能: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(它是一个upvalue 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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

  1. 我们知道,无论lexicalScoperegularFunction不封闭,从上面的定义.
  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"));
    };
}
Run Code Online (Sandbox Code Playgroud)

  1. 我们知道只有上述定义closureFunction的闭包.
  2. 当我们执行程序时,我们希望 message不会被警告,因为它 closureFunction是一个闭包(即它只能在创建函数时访问所有非局部变量(参见本答案) - 这不包括).message
  3. 当我们执行程序时,我们观察message实际上正在被警告.

我们从中推断出什么?

  1. JavaScript解释器不会将闭包与处理其他函数的方式区别对待.
  2. 每个函数都带有它的范围链.闭包没有单独的引用环境.
  3. 闭包就像其他所有功能一样.我们只是在它们所属范围之外的范围内引用它们时才调用它们,因为这是一个有趣的案例.

  • 接受,因为你非常详细,解释了很多正在发生的事情.最后,我现在更好地理解了闭包是什么,或者更好地说:JS中的变量绑定是如何工作的. (40认同)
  • @Peter - 你知道吗,你是对的.常规函数和闭包之间没有区别.我运行了一个测试来证明这一点,它会导致你的青睐:这里是[控制](http://jsfiddle.net/KyQKw/),这里是[替代](http://jsfiddle.net/KyQKw/1/ ).你说的确有意义.JavaScript解释器需要为闭包做特殊的簿记.它们只是具有一流功能的词汇范围语言的副产品.我的知识仅限于我所读的内容(这是假的).谢谢你纠正我.我会更新我的答案以反映同样的情况. (13认同)
  • @AaditMShah我同意你关于闭包是什么的,但你说的好像JavaScript中的*regular*函数和*闭包*之间有区别.没有区别; 在内部,每个函数都会带有对它所在的特定范围链的引用.JS引擎并不认为它是另一种情况.不需要复杂的检查清单; 只知道每个函数对象都有词法范围.变量/属性是全局可用的这一事实并不会使函数成为一个闭包(它只是一个无用的情况). (11认同)
  • 请你说明你的消息来源吗?我从来没有见过一个定义,如果在一个范围内调用,而在另一个范围内调用,则函数可以是闭包.因此,这个定义似乎是我习惯的更一般定义的一个子集(参见[kev的回答](http://stackoverflow.com/a/12930641/508537))其中闭包是一个闭包是一个闭包,无论如何它所称的范围,或者即使它从未被调用过! (9认同)
  • 在案例1中,你说`g`在`setTimeout`的范围内运行,但在案例2中你说`f`在全局范围内运行.它们都在setTimeout内,那有什么区别? (3认同)
  • 如果1`g`作为参数传递给`setTimeout`.因此`g`在`setTimeout`的范围内执行.如果2`f`似乎作为参数传递给`setTimeout`,但要记住`f`是用参数`i`立即调用的.因此它在全局范围内执行,`f`返回的值(即`g`)实际上作为参数传递给`setTimeout`.因此`g`在`setTimeout`的范围内执行. (2认同)
  • 我认为,由于这个术语是通常定义的,第一个程序中的`f`实际上确实接近'i`.也就是说,`f`实际上是对`i`的封闭. (2认同)
  • 这个答案继续传播有关 JavaScript 中闭包的错误信息。JavaScript 中的*每个*函数在创建*PERIOD*时都会形成一个闭包。创建函数时,引用环境始终使用所谓的作用域链存储为函数对象的一部分。我认为人们混淆了闭包何时有用和何时无用之间的区别。 (2认同)

kev*_*kev 94

根据closure定义:

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

您正在使用closure,如果你定义一个使用它的功能之外定义变量的函数.(我们将变量称为自由变量).
他们都使用closure(即使在第一个例子中).


bri*_*out 51

一言以蔽之的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
Run Code Online (Sandbox Code Playgroud)

调用f打印

hello
hello
2
Am I Accessible?
Run Code Online (Sandbox Code Playgroud)

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

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f
Run Code Online (Sandbox Code Playgroud)

我们将调用f词汇父g.如前所述,我们现在有两个范围; 范围f和范围g.

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

关闭

在JavaScript中,函数g不仅可以访问在scope中声明的任何变量,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
Run Code Online (Sandbox Code Playgroud)

调用f打印

hello
undefined
Run Code Online (Sandbox Code Playgroud)

我们来看看这条线console.log(foo);.此时我们在范围内,g并且我们尝试访问foo在范围中声明的变量f.但是如前所述,我们可以访问词法父函数中声明的任何变量,这就是这里的情况; g是词汇的父母f.因此hello被打印.
我们现在看看这条线console.log(bar);.此时我们在范围内,f并且我们尝试访问bar在范围中声明的变量g.bar未在当前作用域中声明且该函数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
Run Code Online (Sandbox Code Playgroud)

然后h将能够访问的功能范围内声明的所有变量h,gf.这是通过闭包完成的.在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
Run Code Online (Sandbox Code Playgroud)

好吧,让我们看看使用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)
  })();
}
Run Code Online (Sandbox Code Playgroud)

因此,这将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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 我认为这是最好的答案,一般简单地解释闭包,然后进入具体的用例.谢谢! (3认同)
  • @leemes,我同意.与接受的答案相比,这并没有真正显示实际发生的事情.:) (2认同)

Eri*_*pen 22

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

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

没有闭包,这会引发错误

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

但引用的上下文实际上是持久的,而不是快照.重复触发返回的内部函数,该函数继续递增并记录外部函数的局部变量将继续警告更高的值.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
Run Code Online (Sandbox Code Playgroud)


Jon*_*Jon 17

你们都在使用闭包.

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

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

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

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

  • @leemes:它抓住了`i`就好了.您描述的行为不是关闭与非关闭的结果; 这是在此期间改变了封闭变量的结果.你通过立即调用一个函数并将`i`作为参数传递(在现场复制其当前值),使用不同的语法做同样的事情.如果你把自己的`setTimeout`放在另一个`setTimeout`里面,那就会发生同样的事情. (6认同)

And*_* D. 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);
}
Run Code Online (Sandbox Code Playgroud)

  • 在您的解决方案中,您创建了20个函数对象(每个循环上有2个对象:2x10 = 20).你的朋友解决方案的结果相同.在"我的"解决方案中,只创建了11个函数对象:1个用于循环,10个用于"内部" - 1 + 1x10 = 11.结果 - 减少了内存使用并提高了速度. (3认同)

Ja͢*_*͢ck 10

让我们看看两种方式:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();
Run Code Online (Sandbox Code Playgroud)

声明并立即执行setTimeout()在其自己的上下文中运行的匿名函数.i通过将副本放入i2第一个来保留当前值; 它的工作原理是因为立即执行.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);
Run Code Online (Sandbox Code Playgroud)

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

重要

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

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

结论

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


And*_*ies 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)
}
Run Code Online (Sandbox Code Playgroud)

所有人都在使用闭包.不要将执行点与闭包混淆.如果闭包的'快照'是在错误的时刻拍摄的,那么这些值可能是意料之外的,但肯定会关闭!


小智 8

我刚才写了这篇文章,提醒自己一个闭包是什么以及它在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' 
Run Code Online (Sandbox Code Playgroud)


Ram*_*esh 6

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

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

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

  • 使用右侧面板中的"Closure"部分,因为没有更具体的名称."本地"比"关闭"更强烈. (2认同)