【问题标题】:Does a Javascript closure retain the entire parent lexical environment or only the subset of values the closure references? [duplicate]Javascript 闭包是保留整个父词法环境还是仅保留闭包引用的值的子集? [复制]
【发布时间】:2019-12-11 01:29:28
【问题描述】:

考虑以下示例:

function makeFunction() {
  let x = 3;
  let s = "giant string, 100 MB in size";

  return () => { console.log(x); };
}

// Are both x and s held in memory here
// or only x, because only x was referred to by the closure returned
// from makeFunction?
let made = makeFunction();

// Suppose there are no further usages of makeFunction after this point

// Let's assume there's a thorough GC run here

// Is s from makeFunction still around here, even though made doesn't use it?
made();

因此,如果我只关闭父词法环境中的一个变量,该变量是否保留,或者其词法环境中的每个同级变量也保留?

另外,如果 makeFunction 本身嵌套在另一个外部函数中,即使 makeFunction 和 makeFunction 的返回值都没有引用该外部词法环境中的任何内容,是否会保留该外部词法环境?

我问的是性能原因 - 闭包是保留一堆东西还是只保留它们直接引用的东西?这会影响内存使用和资源使用(例如打开的连接、句柄等)。

这主要是在 NodeJS 上下文中,但也可以在浏览器中应用。

【问题讨论】:

  • 当然,如果您使用eval 或一些花哨的arguments 调用,则必须保留整个词法范围,因此默认情况下闭包不能仅保留对明确引用的内容的引用.此外,如果您访问任何非常量全局变量,这些全局变量稍后可能是 eval(或更改为 eval)。但是,在某些情况下,个别引擎可能会优化掉引用(如果没有使用有趣的东西)。我也很想知道这个问题的答案。
  • 这取决于浏览器和实现。 Afaik 大多数现代浏览器都试图不保留它。但不能保证。
  • 您有什么性能原因要问?函数执行后,该范围内所有未引用的内存都应该被释放。垃圾回收是 JavaScript 的一个内部特性;使用这种语言进行开发时,您实际上不必考虑它。您可以在此处阅读有关内存管理的信息:developer.mozilla.org/en-US/docs/Web/JavaScript/…
  • 尝试使用console.dir(made); 并深入了解[[Closure]] 属性以查看它保留的内容。

标签: javascript node.js v8


【解决方案1】:

V8 开发人员在这里。这有点复杂;-)

简短的回答是:闭包只保留他们需要的东西。

因此,在您的示例中,makeFunction 运行后,s 引用的字符串将有资格进行垃圾回收。由于垃圾收集的工作方式,无法预测它何时会被释放。 “在下一个垃圾收集周期”。 makeFunction 是否再次运行无关紧要;如果它再次运行,将分配一个新字符串(假设它是动态计算的;如果它是源中的文字,那么它会被缓存)。 made 是否已经运行或将再次运行也无关紧要;重要的是你有一个引用它的变量,所以你可以运行它(再次)。引擎通常无法预测哪些函数会在未来执行或不会执行。

更长的答案是有一些脚注。一方面,正如 cmets 已经指出的那样,如果您的闭包使用 eval,那么一切都必须保留,因为无论源 sn-p 是 eval'ed 都可以引用任何变量。 (但是,关于可能引用eval 的全局变量的评论不是真的;“全局评估”(又名“间接评估”)存在语义差异:它看不到局部变量。这通常被认为是一个优势为了性能和可调试性——但更好的是根本不使用eval。)

另一个脚注有点不幸的是,跟踪并没有尽可能细粒度:每个闭包将保持任何闭包的需求。我们已经尝试解决这个问题,但事实证明,更细粒度的跟踪会导致更多的内存消耗(用于元数据)和 CPU 消耗(用于完成工作),因此对于实际代码通常不值得(尽管它可能会对人工测试正是强调这种情况)。举个例子:

function makeFunction() {
  let x = 3;
  let s = "giant string, 100 MB in size";
  let short_lived = function() { console.log(s.length); }
  // short_lived();  // Call this or don't, doesn't matter.
  return function long_lived() { console.log(x); };
}

let long_lived = makeFunction();

在这个修改后的例子中,尽管long_lived 只使用了xshort_lived 确实使用了s(即使它从未被调用!),并且只有一个桶用于“来自makeFunction 的局部变量这是一些关闭所需要的”,这样存储桶就可以同时保持xs 活着。但正如我之前所说:真正的代码很少会遇到这个问题,所以这通常是你不必担心的。

旁注:

还有资源使用情况(例如打开的连接、句柄等)

作为一个非常笼统的陈述(即,在任何语言或运行时环境中,无论闭包或其他什么),通常建议不要依赖垃圾收集来进行资源管理。我建议在合适的时候立即手动明确地释放资源。

【讨论】:

  • 引擎通常无法预测哪些函数会在未来执行或不会被执行” - 除了没有从任何地方引用的函数之外,那些已知不会被执行的函数再次执行:-)
  • @Bergi:当然,零引用的函数(就像任何其他对象一样)可以被垃圾收集器找到并释放;然而,这对 OP 的问题没有帮助,因为是否需要在解析时做出给定变量是否需要上下文分配的决定,并且根据定义,解析器对程序语义的理解非常肤浅——尤其是,它没有不知道哪些东西在别处被引用,哪些东西没有。
猜你喜欢
  • 2013-09-23
  • 1970-01-01
  • 1970-01-01
  • 2012-01-24
  • 2016-06-15
  • 1970-01-01
  • 2014-07-14
  • 1970-01-01
  • 2020-07-03
相关资源
最近更新 更多