JavaScript 闭包与匿名函数

IT技术 javascript scope closures
2021-02-02 16:30:11

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

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

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

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

6个回答

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

闭包的简单解释:

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

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

案例 1:您朋友的程序

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(这是用于的upvalue 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(这是用于的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"));
    }
}

  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"));
    };
}

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

我们从中推断出什么?

  1. JavaScript 解释器不会以对待其他函数的方式来对待闭包。
  2. 每个函数都带有它的作用域链闭包没有单独的引用环境。
  3. 闭包就像其他所有函数一样。当它们它们所属的作用域之外的作用域中引用时,我们就称它们为闭包,因为这是一个有趣的例子。
@AaditMShah 我同意你关于闭包是什么的看法,但你说的好像JavaScript 中的常规函数和闭包之间存在差异没有区别; 在内部,每个函数都带有对创建它的特定作用域链的引用。JS 引擎不认为这是不同的情况。不需要复杂的清单;只知道每个函数对象都带有词法作用域。变量/属性是全局可用的这一事实并没有使函数不再是一个闭包(这只是一个无用的情况)。
2021-03-20 16:30:11
在案例 1 中,您说它g在 的范围内运行setTimeout,但在案例 2 中,您说它f在全局范围内运行。它们都在setTimeout内,那么有什么区别呢?
2021-03-21 16:30:11
@Peter - 你知道吗,你是对的。常规函数和闭包之间没有区别。我进行了一个测试来证明这一点,结果对您有利:这是控制,这是替代你说的有道理。JavaScript 解释器需要为闭包做特殊的簿记。它们只是具有一流功能的词法作用域语言的副产品。我的知识仅限于我阅读的内容(这是错误的)。谢谢你纠正我。我会更新我的答案以反映相同的情况。
2021-03-23 16:30:11
你能说明你的消息来源吗?我从未见过这样的定义:如果在一个作用域中调用函数而不是在另一个作用域中调用,则该函数可以是闭包。因此,这个定义似乎是我习惯的更一般定义的一个子集(参见kev 的回答),其中闭包是一个闭包,无论它被调用的范围如何,或者即使它从未被调用过!
2021-03-30 16:30:11
接受,因为你非常详细,很好地解释了正在发生的事情。最后,我现在更好地理解了闭包是什么,或者更好地说:变量绑定在 JS 中是如何工作的。
2021-04-07 16:30:11

根据closure定义:

A“封闭”是可以具有一个表达式(通常是功能)自由变量与一起环境结合这些变量(即“关闭”的表述)。

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

@Jon 返回的函数使用i2在外部定义。
2021-03-23 16:30:11
@kev 如果您定义了一个使用在函数外部定义的变量的函数,那么您正在使用闭包......那么在“Aadit M Shah”的“案例 1:您朋友的程序”中,答案是“函数 f”关闭?它使用 i (在函数外部定义的变量)。全局范围是否引用了一个限定符?
2021-04-07 16:30:11
第三个版本如何使用函数外定义的变量?
2021-04-10 16:30:11

一言以蔽之的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

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

窗口对象

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

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

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

两种尝试都使用闭包

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

让我们创建一个新的编程语言;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 中对象访问

为什么应该i是未定义的?您只需引用父作用域,如果没有闭包,它仍然有效。
2021-03-18 16:30:11
我认为这是最好的答案,一般而简单地解释了闭包,然后进入了特定的用例。谢谢!
2021-03-18 16:30:11
@leemes,我同意。与接受的答案相比,这并没有真正显示实际发生了什么。:)
2021-03-22 16:30:11
与 foo 在 JavaScript-No-Closure 中未定义的原因相同。<code>i</code> 在 JavaScript 中不是未定义的,这要归功于 JavaScript 中允许访问词法父级中定义的变量的功能。此功能称为闭包。
2021-04-02 16:30:11
你不明白引用已经定义的变量和自由变量之间的区别在闭包中,我们定义了必须绑定在外部上下文中的自由变量。在您的代码中,您只需在定义函数时设置 i2i这使得iNOT 成为自由变量。尽管如此,我们认为您的函数是一个闭包,但没有任何自由变量,这就是重点。
2021-04-06 16:30:11

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

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

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

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

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

一旦outerFunc 在假想的禁用闭包的JavaScript 版本中返回,对outerVar 的引用将被垃圾收集并消失,没有留下任何可供内部func 引用的内容。

闭包本质上是一种特殊的规则,当内部函数引用外部函数的变量时,这些规则可以让这些变量存在。使用闭包,即使在外部函数完成或“关闭”之后,引用的变量也会保留,如果这有助于您记住这一点。

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

一旦您在内部函数中引用了外部变量,就像门框被放置在垃圾收集方式中,这些被引用的变量。

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

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

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
您对“快照”(我认为,您参考我的答案)的看法是正确的。我正在寻找一个词来表示这种行为。在您的示例中,它可以被视为“热链接”闭包结构。当在内部函数中捕获闭包作为参数时,可以将其声明为“快照”。但我同意,误用的词只会增加主题的混乱。如果您对此有任何建议,我会更新我的答案。
2021-03-12 16:30:11
嗯……说得好。引用未定义的 var 是否永远不会抛出错误,因为它最终会作为全局对象的属性查找,或者我是否对未定义的 var 赋值感到困惑?
2021-03-23 16:30:11
如果您将内部函数指定为命名函数,则可能有助于解释。
2021-04-05 16:30:11
如果没有闭包,你会得到一个错误,因为你试图使用一个不存在的变量。
2021-04-10 16:30:11

你们都在使用闭包。

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

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

您朋友的尝试显然使用了i非本地变量,方法是获取它的值并制作一个副本以存储到 local 中i2

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

是的,但我认为这一点是如何,他是做什么的。他只是复制ii2,然后定义一些逻辑并执行这个函数。如果我不立即执行它,而是将它存储在一个 var 中,并在循环之后执行它,它会打印 10,不是吗?所以它并没有抓住我。
2021-03-29 16:30:11
@leemes:它捕获得i很好。您所描述的行为不是封闭与非封闭的结果;这是封闭变量在此期间改变的结果。通过立即调用函数并i作为参数传递(当场复制其当前值),您正在使用不同的语法做同样的事情如果你把自己的setTimeout放在另一个里面setTimeout,同样的事情也会发生。
2021-04-03 16:30:11