JavaScript匿名函数的生命周期是多久?

17

如果我在全局作用域中写下以下代码:

(function(){})();

当语句执行时创建的匿名函数,执行完语句后立即销毁,对吗?

如果我把这个写在一个函数里:

function foo()
{
    var a=1;
    (function(){})();
    a++;
}

匿名函数是在foo函数返回之前存在还是仅在执行该语句时存在?


我的第一个猜测是,在语句被垃圾回收器执行后,它会被销毁。因为之后没有保留到那段代码的链接。我会等待有证据的答案 :) 在这里查看 - Orelsanpls
1
是的,函数像其他对象一样被创建和垃圾回收。 - Bergi
2个回答

22

在这种情况下,大多数引擎都会完全优化掉该函数,因为它没有任何作用。

但是假设该函数包含代码并确实被执行。在这种情况下,该函数将一直存在,无论是作为已编译的代码、字节码还是解释器的AST。

不会一直存在的部分是作用域和可能创建的闭包。为该函数创建的作用域和闭包仅在函数执行或具有特定绑定作用域/闭包的函数引用存在时才存在。

因此,组合 函数引用+作用域 将在执行语句 (function(){})(); 时分配,并可以在该语句之后释放。但是编译后的 function(){} 版本可能仍然存在于内存中以供以后使用。

对于进行即时编译和优化的引擎,一个函数甚至可能存在于不同的编译版本中。

现代js引擎的JIT+优化器部分是一个复杂的主题,可以在这里找到v8的粗略描述:html5rocks:JavaScript Compilation

在V8中,全编译器将运行所有代码,并尽快开始执行代码,快速生成良好但不是最佳的代码。该编译器在编译时几乎不假设类型-它预计变量类型可能会在运行时更改。

与全编译器并行,V8使用优化编译器重新编译“热”函数(即运行多次的函数)。[...] 在优化编译器中,操作被推测地内联(直接放置在调用它们的位置)。这加快了执行速度(以内存占用为代价),但也启用了其他优化。

因此,生成的代码可能与原始代码几乎没有相似之处。

因此,立即调用的函数表达式甚至可以使用内联完全优化掉。


2
关于V8的信息已经过时约17个月了,自从用Ignition+TurboFan替换了旧的Full-codegen+Crankshaft后,V8已经使用了一种字节码解释器来处理一次性或很少使用的代码(详情请见:https://v8.dev/blog/launching-ignition-and-turbofan)。(Ignition是字节码解释器。) - T.J. Crowder
2
我也不确定一次性函数的字节码是否会被丢弃,V8团队一直在努力减少内存消耗;删除所有关于永远不会再次调用的函数的信息将是易如反掌的事情。 - T.J. Crowder
@T.J.Crowder,没错,我正要更新一下如果字节码未被使用,它也很可能会被释放的那一部分内容。就像现有代码在特定方式下被优化一样。我会更新V8信息。 - t.niese

5
如果我在全局作用域中写下以下代码:
(function(){})();

当语句被执行时,创建匿名函数并在语句执行后立即销毁?

正如t.niese所说,引擎很有可能会完全优化掉这个函数。因此,让我们假设它包含一些代码:

// At global scope
(function(){ console.log("Hi there"); })();

引擎无法保证该代码不会引发错误(例如,如果将console替换为其他内容),因此我相当确定它不能直接内联。

现在的答案是:视情况而定

从语言/规范层面上来说,在编译单元(大致上指脚本)中的所有代码都会在第一次加载编译单元时进行解析。然后,在代码逐步执行到该函数时创建函数,执行并立即合格以进行垃圾回收(连同执行环境)。但这只是理论/高级规范。

从JavaScript引擎的角度来看:

  • 在任何代码运行之前,函数都会被解析。该解析的结果(字节码或机器码)将与该函数表达式相关联。这不会等待执行到达该函数,而是提前完成的(在V8 [Chrome和Node.js中的Google引擎]上在后台完成)。
  • 一旦函数已经执行,且没有其他东西可以引用它:
  • 函数对象和调用它所涉及的执行环境都有资格进行GC。何时以及如何进行取决于JavaScript引擎。
  • 这留下了函数的底层代码,无论是字节码(现代版本的V8使用Ignition,可能还有其他引擎)还是编译后的机器码(某种程度上函数被使用得如此频繁以至于它已经为TurboFan编译,或者旧版的V8使用Full-codegen,其他引擎)。JavaScript引擎是否可以放弃生成函数的字节码或机器码将取决于引擎。如果它们可能需要再次使用,我怀疑引擎不会丢弃它们所生成的字节码/机器码(例如,对通过对foo的新调用创建的新匿名函数)。如果 foo 变得不可访问,则可以将 foo 的字节码/机器码和匿名函数的字节码/机器码视为不必要而被丢弃。我不知道引擎是否会这样做。一方面,它似乎是低垂的果实;另一方面,它似乎是如此罕见,以至于不值得费心。 (请记住,在这里,我们谈论的不是附加到函数实例的代码,而是从创建实例时附加到实例的源代码产生的代码,并且随着时间的推移可能会附加到多个实例。)

以下是V8博客上的几篇有趣的文章:

如果我将这写在一个函数中:

function foo()
{
    var a=1;
    (function(){})();
    a++;
}
匿名函数存在于 foo 函数运行期间还是只在执行那条语句期间?假设该函数中有一个 console.log,我认为它依赖于一个全局可写的变量(console),这就意味着它不能被内联。从高层次的概念上讲,答案是一样的:当脚本加载时,该函数被解析,到达时创建,执行,并在完成执行后进行垃圾回收。但这只是高层次的概念。在引擎层面,情况可能会有所不同:
- 代码将在脚本中的任何代码之前进行解析。 - 字节码或机器码可能会在脚本中的任何代码之前生成,尽管我似乎记得来自 V8 博客的某些内容关于解析而不是立即编译顶级函数的内容。不过如果不仅仅是我想象中的话,我找不到那篇文章。 - 当执行到该函数时,会为其创建一个函数对象以及执行上下文(除非引擎确信它可以优化掉而不在代码中观察到)。 - 执行结束后,函数对象和执行上下文都可以进行垃圾回收。(它们很可能已经在堆栈上,所以当 foo 返回时 GC 很容易。) - 然而,底层代码仍然留存在内存中以便再次使用(如果经常使用,将进行优化)。

1
“解析但不立即编译顶层函数的内容” - 这不仅存在于你的脑海中,我也记得。当然,我也无法链接到那篇文章... - Bergi
“永远无法再次到达的函数”:如果foo可以再次调用,则匿名函数肯定可以再次到达。我希望SpiderMonkey、V8和JavaScriptCore知道这一点,并且不会垃圾回收这些函数。如果不是这样,那么最好的做法就是不要在频繁调用的代码中使用匿名函数。 - Inigo
@Inigo - 如果你再次调用 foo,它就是一个不同的匿名函数,而不是同一个函数。但是这条评论让我想稍微修改一下关于字节码/机器码的段落。 - T.J. Crowder
是的,从语言语义角度来看,它与其他语言有所不同,但从解析器/词法分析器和编译代码(无论是字节码还是JIT本地代码)的角度来看并没有区别。你对语言规范和引擎之间的区别的划分使得你的回答比被接受的回答更好!考虑到JavaScript中匿名函数用于回调和闭包的普遍使用,我不确定这是否真的那么“罕见”,尽管可能不足以进行优化?也许通过你的拉取请求,你可以让V8或Firefox开发人员参与进来? - Inigo
@Inigo - 哈哈,我没有影响力。 :-) 我认为我已经做出了区分(现在更好了,因为你的评论),所以我认为我们没问题。 - T.J. Crowder
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接