【问题标题】:Do variables need to be referenced for them to be included in a closure?是否需要引用变量才能将它们包含在闭包中?
【发布时间】:2023-03-11 00:29:01
【问题描述】:

在创建闭包时(在 Javascript 或 C# 中),在创建闭包时范围内的所有变量是否都“封闭”在其中?还是只是新创建的方法中引用的变量?

示例 C# 代码:

private void Main() {
    var referenced = 1;
    var notReferenced = 2;  // Will this be enclosed?
    new int[1].Select(x => referenced);
}

示例 Javascript 代码:

    var referenced = 1;
    var notReferenced = 2;  // Will this be enclosed?
    var func = function () {
        alert(referenced);
    }

(通过使用 Javascript 闭包创建循环引用来阅读 IE 中的内存泄漏时,我遇到了这个问题。http://jibbering.com/faq/notes/closures/#clMem
注意:使用“封闭”一词,我的意思是 MSDN 所说的“捕获”。 (http://msdn.microsoft.com/en-us/library/bb397687.aspx)

【问题讨论】:

  • 您必须在 C# 的上下文中定义“封闭”的含义,这不是我所知道的术语。
  • 至少对于 JavaScript,这可以很好地依赖于实现。
  • 什么意思?编译器实际上只生成“闭包”代码来引用需要访问的变量。但是您可以认为这是一种优化。从概念上讲,您可能会认为所有可见的东西都在函数的闭包中,无论它是否被引用。
  • 有关 JavaScript 的更多最新答案,请参阅 stackoverflow.com/questions/5368048/…

标签: c# javascript closures


【解决方案1】:

您有两个问题。将来,当您有两个问题时,您可能会考虑发布两个单独的问题。

在 Javascript 中创建闭包时,创建闭包时范围内的所有变量是否都“封闭”在其中?

您没有说明您正在谈论的“JavaScript”的多个版本中的哪一个。我将假设您正在谈论 ECMAScript 3 语言的正确实现。如果您在谈论其他版本的“JavaScript”,请说出您在谈论哪个版本。

ECMAScript 5 更新了关于词法环境和“eval”如何工作的规则。自 2001 年以来,我就不再是 Technical Committee 39 的成员,并且我没有及时了解 ECMAScript 5 规范的最新变化;如果您想在最近的 ECMAScript 5 规则的上下文中得到答案,请查找该规范的专家;我不是。

在 ECMAScript 3 的上下文中,您的问题的答案是。 ECMAScript 3 规范在这一点上很清楚,但你不需要看规范就知道一定是这样的:

function f()
{
 var referenced = 1;
 var notReferenced = 2;
 var func = function () 
 {
   alert(referenced);
   alert(eval("notReferenced"));
 }
 return func;
}
f()();

如果未捕获“notReferenced”,“eval”如何正常工作?

这在 ECMAScript 规范中都有解释,但我可以在这里简单总结一下。

每个变量都与一个“变量对象”相关联,该对象是具有名称为变量名称的属性的对象。函数 f 的 变量对象 与 f 的 activation 对象 相同——即每次调用 f 时都会神奇地创建的对象。变量对象具有三个属性:“referenced”、“notReferenced”和“func”。

有一个变量对象叫做“全局对象”,它代表任何函数之外的代码。它有一个属性“f”。

每个执行上下文都有一个作用域链,它是在尝试评估标识符时搜索其属性的对象列表。

每个函数对象都有一个与之关联的作用域链,它是创建函数时有效的作用域链的副本。

与“f”关联的作用域链是全局对象。

当执行进入“f”时,执行上下文的当前作用域链将“f”的激活对象推送到它上面。

当“f”创建分配给“func”的函数对象时,其关联的作用域链是执行上下文的当前作用域链的副本——即包含“f”激活的作用域链,和全局对象。

好的,现在所有对象都已正确设置。 f 返回 func,然后调用它。这会为该函数创建一个激活对象。执行上下文的作用域链是从函数对象中获取的——记住,它是“f”的激活对象加上全局对象——然后我们将当前激活对象推送到作用域链的副本上。由于我们现在执行的匿名函数既没有参数也没有局部变量,这本质上是一个空操作。

然后我们尝试评估“警报”;我们查找范围链。当前激活和“f”激活没有任何称为“alert”的东西,所以我们询问全局对象,它说是,alert 是一个函数。然后我们评估“引用”。它不在当前激活对象上,而是在 f 的激活对象上,所以我们获取它的值并将其传递给警报。

下一行也一样。全局对象告诉我们有方法“alert”和“eval”。但当然“eval”是特殊的。 Eval 获取当前作用域链的副本以及字符串参数。 Eval 将字符串解析为程序,然后使用当前作用域链执行程序。该程序查找“notReferenced”并找到它,因为它位于当前作用域链上。

如果您对此领域有更多疑问,那么我鼓励您阅读 ECMAScript 3 规范的第 10 章和第 13 章,直到您彻底理解它们。

让我们更深入地看看你的问题:

在 Javascript 中创建闭包时,在创建闭包时范围内的所有变量是否都“封闭”在其中?

要明确回答这个问题,您需要准确地告诉我们您所说的“闭包”是什么意思——ECMAScript 3 规范没有在任何地方定义该术语。 “闭包”是指函数对象还是函数对象捕获的作用域链

记住,所有这些对象都是可变的——作用域链本身是不可变的,但是链中的每个对象都是可变的!在创建作用域链或函数对象的“当时”是否存在变量实际上有点无关紧要;变数来,变数去。

例如,可以捕获在函数对象创建时尚未创建的变量!

function f()
{
 var func = function () 
 {
   alert(newlyCreated);
 }
 eval("var newlyCreated = 123;");
 return func;
}
f()(); 

显然,“newlyCreated”在函数对象创建时不是一个变量,因为它是在函数对象之后创建的。但是当函数对象被调用时,它就在 f 的激活/变量对象上。

同样,在创建嵌套函数对象时存在的变量可以在函数对象执行时被删除。

【讨论】:

  • 我认为 eval 在评估和评估 "var newlyCreated = 123;" 时会获得一个新创建的框架,并将在该框架中定义它(而不是 f 的框架)。我猜我错了。
  • @configurator:我建议您参考 E3 规范的第 10.2.2 节:其中指出“变量实例化是使用调用上下文的变量对象执行的......
  • @Raynos:你怎么知道在解析时是否提到了“eval”?在 parse 的时候你是不知道的;您只能在 运行 时知道它。在 E3 中,在全局范围内说 var whatever="hello"; window[whatever] = eval; 是完全合法的,然后在完全不同的函数中说 hello("code"); ,嘿,helloeval 的同义词。也许在 E5 中你不允许用 eval 做疯狂的事情;我还没有阅读规范。但在 E3 中,你绝对可以这样做。
  • @protector one:不。不同的语言是不同的。
  • @Eric Lippert:这在 Chrome 中失败并出现 ReferenceError(x 未定义),支持 @Raynos 所说的以及不可能的。 (function(a,b,c,d){var x=1;return window[a+b+c+d]('x');})('e','v','a','l');
【解决方案2】:

您有两个问题。将来,当您有两个问题时,您可能会考虑发布两个单独的问题。

在 C# 中创建闭包时,创建闭包时范围内的所有变量是否都“封闭”在其中?还是只是新创建的方法中引用的变量?

这取决于您是要求法律上还是事实上答案。

首先,让我们澄清您的问题。我们将“外部变量”定义为发生在方法内部但在任何 lambda 或匿名方法之外的实例方法的局部变量、值参数(即不是 ref 或 out)、“params”参数或“this”该方法内部的表达式。 (是的,“this”被归类为“外部变量”,尽管它没有被归类为“变量”。这很不幸,但我已经学会了忍受它。)

在 lambda 或匿名方法表达式内部使用的外部变量称为“捕获的外部变量”。

所以现在让我们重新表述问题:

在运行时创建对应于 lambda 或匿名方法的委托时,所有在委托创建时范围内的外部变量的生命周期是否扩展为匹配(或超过) 代表的生命周期?或者只是捕获外部变量的生命周期延长了?

事实上,只有捕获的外部变量的生命周期得到了延长。 法律上,规范要求延长捕获的外部变量的生命周期,但没有禁止延长未捕获的外部变量的生命周期.

请阅读 C# 4 规范的第 5.1.7、6.5.1 和 7.15.5 部分了解所有详细信息。

正如 Jon 所指出的,我们在确保最小化捕获的外部变量的生命周期方面做得不是特别好。我们希望有一天能做得更好。

【讨论】:

    【解决方案3】:

    我只能回答 C# 方面的问题。

    如果变量实际上没有被闭包捕获,它将作为普通的局部变量保留在方法中。如果它被捕获,它只会被提升为合成类中的实例变量。 (当您谈论“封闭”变量时,我假设这就是您所说的。)

    但是请注意,如果两个 lambda 表达式各自捕获相同范围的不同变量,那么在当前实现中两个 lambda 将使用相同的合成类:

    private Action Foo() {
        var expensive = ...;
        var cheap = ...;
        Action temp = () => Console.WriteLine(expensive);
        return () => Console.WriteLine(cheap);
    }
    

    这里的 returned 操作仍将保持“昂贵”的引用有效。然而,所有这些都是 Microsoft C# 4 编译器的实现细节,将来可能会发生变化。

    【讨论】:

    • "enclosed" 表示闭包关闭变量。因此,变量是并且保持在该特定闭包的范围内。从它的声音来看,您的合成类和捕获匹配。
    • @Raynos:尽管用 C# 术语,这种描述实际上并不适用。每种语言都可以以略微不同的方式使用术语——这就是为什么这是一个有点奇怪的问题,试图涵盖两种语言。在未引用的变量被封闭的情况下,可观察的变化是什么?
    • 在封闭的未引用变量方面,我能想到的唯一“可观察到的变化”变化是,在封闭被清理之前,GC 无法清理封闭的未引用变量。实现这一点基本上是实现了一个较弱的 GC。我认为这样做没有任何价值,但它又是特定于实现的。
    • @Raynos:对 - 好吧,在那种情况下,我认为我们正在谈论同一件事,而我的答案可能是 OP 在 C# 方面所寻找的.
    • Jon 提到的问题在this question 中有更详细的讨论,我在其中明确询问过。
    【解决方案4】:

    我只能回答关于 C# 的问题:

    不,它不会被封闭,因为它不是必需的。

    我解释“它会被封闭吗?”在这种情况下,如下所示:

    是否会在为 lambda 表达式生成的闭包中被捕获,即自动生成的类中是否会有一个字段包含此变量的值?

    免责声明:
    这是一个实现细节。对于您的具体情况,MS 当前实现的 C# 编译器的答案是正确的。 Mono 编译器或更新版本的 MS 编译器可能会有所不同。或者 - 正如乔恩的回答所示 - 对于更复杂的示例,它可能会有所不同。

    【讨论】:

    • 如果它是一个实现细节,您能否参考 C# ECMA 规范中的相关信息,表明该规范并不关心任何一种方式。
    • @Raynos:不,因为没有针对 C# 3.0 或 C# 4.0 的 ECMA 规范,请参见此处:en.wikipedia.org/wiki/C_Sharp_(programming_language)#Versions 并且 C# 2.0 的 ECMA 规范自然没有提及任何关于 lambda 或闭包的内容。
    • 感谢您指出这一点,我实际上希望规范能够保持最新。
    • 您可以查看维基百科文章第 7.15.5 章中链接的 MS 规范
    • @Raynos:您可以阅读 ECMA C# 2 规范的第 14.5.15 节;它详细介绍了为匿名方法实现定义的所有不同事物。对于 C# 3 和 4 中的 lambda,所有这些仍然是实现定义的。特别是参见第 14.5.15.5 节的第一句。
    【解决方案5】:

    在创建闭包时(在 Javascript 或 C# 中),在创建闭包时范围内的所有变量是否都“封闭”在其中?还是只是新创建的方法中引用的变量?

    我只能回答关于 JavaScript 的问题。

    这是特定于实现的。一些引擎包含所有变量。一些引擎优化并且只包含引用的变量。

    我知道 Chrome 会优化并且只包含它关心的变量。

    进一步阅读:

    这个问题似乎也源于对旧 IE 内存泄漏问题的关注。这在现代浏览器中不是问题。

    通过在 EcmaScript 和 Host 对象之间进行循环引用,深入了解 IE8 中仍然存在的内存泄漏:

    IE8 memory leak

    为了澄清闭包的 IE 内存泄漏问题,主要是关于主机对象 (DOM) 和 JavaScript 之间的循环引用。所以这个问题不存在,除非你使用 DOM(el.attachEvent 或类似的东西)

    【讨论】:

    • 天哪,我担心可能是这种情况......
    猜你喜欢
    • 1970-01-01
    • 2019-03-18
    • 1970-01-01
    • 2015-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-13
    相关资源
    最近更新 更多