JavaScript 专家:带有 `{}` 的块作用域和匿名函数是否都有助于垃圾收集?

JavaScript experts: Do block-scopes with `{}` and anonymous functions both help garbage-collection?

在书 "You don't know JS: scopes & closures" 中,Kyle simpson 指出块作用域变量有助于垃圾回收,这里是具体示例:

function process(data) {
// do something interesting
}

{
let someReallyBigData = {};
process(someReallyBigData);
}

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("Clicked!");    
}, false);

现在上面的例子应该有助于垃圾收集,因为变量 someReallyBigData 会在块结束后立即从内存中删除,不像这个例子,它对垃圾收集没有帮助:

function process(data) {
// do something interesting
}

var someReallyBigData = {};

process(someReallyBigData);

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("Clicked!");    
}, false);

现在我确信这个人对他提供的例子(第一个)是正确的;但是,我想知道如果我们使用匿名 IIFE(立即调用的函数表达式)以及正常的 var 而不是 {} 大括号和 let 多变的。让我把它变成一个例子:

function process(data) {
// do something interesting
}

(function(){
var someReallyBigData = {};
process(someReallyBigData);
}());

var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("Clicked!");    
}, false);

从表面上看,他们应该做同样的事情;因为就像块范围的 someReallyBigData 变量在代码块执行后不能再被任何东西访问一样,匿名函数中的代码一旦执行就不能被任何东西访问,任何东西,从任何地方。

那么,它们真的对 Javascript 引擎的垃圾收集机制有同样的影响吗?我几乎可以肯定是这种情况,直到我用谷歌搜索 "anonymous function garbage-collection" 并且出现的几乎所有 material 都只说负面的事情,例如 "anonymous functions cause memory leaks" 等等

如果有人能阐明这件事,我会很高兴。

请不要忘记我的问题有点针对我提供的示例,谢谢!

当您在声明中使用 letconst 关键字时,

JavaScript 仅具有块级范围。仅仅因为你有 {} 并不会创建块级范围(就像大多数其他语言的情况一样)。

除此之外,垃圾回收依赖于实现,您很可能不会注意到块作用域导致的任何性能差异。

匿名函数可能会对垃圾回收产生影响,因为可以以这样一种方式设置函数,即不必为以后可能的调用而存储它。一个很好的例子是一个函数只需要 运行 一次(即当文档被完全解析时):

window.addEventListener("DOMContentLoaded", function(){ . . . });

但是,这并不意味着所有匿名函数都提供此优势,因为函数最终可能会被存储(即,如果它从函数返回,然后在变量中捕获)或 如果匿名函数设置闭包,然后所有赌注都关闭了。

另外,请注意,您不能像对命名函数进行单元测试那样简单地对匿名函数进行单元测试。

I am wondering whether or not everything would be the same if we used an anonymous IIFE

这当然是可能的,这也是转译器用来模拟块作用域的方法。然而 IIFE 看起来有点笨拙,带有 let/const 变量的块作用域 更易于使用 。另见

Now the first example is supposed to help with garbage-collection since the variable someReallyBigData will be dropped from memory as soon as the block ends, unlike the second example, which doesn't help with garbage-collection.

请注意,这个词是 helps,而不是 enables。今天的引擎 can garbage-collect the variable just fine,因为他们的优化器发现它没有在保留的闭包中使用。块范围仅使这种静态分析更容易

(这里是 V8 开发人员。)是的,有几种方法可以使对象无法访问,至少包括以下所有方法:

  • 将内容放入 let 块作用域中声明的变量
  • 将内容放入 IIFE
  • 用完后清除变量(varlet):someReallyBigData = null;

最终结果在所有情况下都是相同的:不再可达的对象有资格进行垃圾回收。

基于此处讨论的其他注释:

  • 问题中引用的建议对顶级代码有意义。在一个合理大小的函数中,我不会担心它——函数可能 return 很快就没有区别,所以你不需要为这些考虑而增加负担。

  • "an object can be freed now"和"an object will be freed now"有很大区别。让某些东西超出范围不会导致它立即被释放,也不会导致垃圾收集器 运行 更频繁。这只是意味着每当垃圾收集器下一次决定去寻找垃圾时,有问题的对象将符合条件。

  • 无论是否匿名,IIFE 都是 IIFE。示例:

    (function I_have_a_name() {
      var someReallyBigData = ...;
    })();
    // someReallyBigData can be collected now.
    I_have_a_name();  // ReferenceError: I_have_a_name is not defined
    
  • 创建闭包本身并不能使事情保持活力。但是,如果闭包在其外部范围内引用变量,那么(当然!)只要闭包存在,就无法收集这些变量。示例:

    var closure = (function() {
      var big_data_1 = ...;
      var big_data_2 = ...;
      return function() { return big_data_1.foo; }
    })();
    // big_data_2 can be collected at this point.
    closure();  // This needs big_data_1.
    // big_data_1 still cannot be collected, closure might need it again.
    closure = null;
    // big_data_1 can be collected now.
    
  • 优化编译器对这一切影响不大。它通常在每个函数的基础上运行,并且通常不会优化顶层(因为大多数逻辑往往在函数中)。 一个函数中,优化编译器非常了解对象的生命周期(这是优化编译器的一部分)。