JavaScript闭包与未引用变量的相关问题

8

我知道有关闭包的优秀文章在这里在这里,但似乎都没有涉及到我所考虑的特殊情况。这个问题最好通过代码演示:

function foo() {
    var x = {};
    var y = "whatever";

    return function bar() {
        alert(y);
    };
}

var z = foo();

参考在bar中引用y会调用一个闭包,只要我保留z,垃圾回收器就不会清除y。问题是——x会发生什么?即使它没有被引用,它也会被该闭包持有吗?垃圾回收器会看到没有x的引用并清理它吗?或者只要我持有zx就会与y一起存在?(理想的答案将引用ECMA规范。)

1个回答

11
问题是--x发生了什么?
答案因理论和实现而异。
在理论上,是的,x保持活动状态,因为闭包(匿名函数)引用了调用foo时上下文的绑定对象,其中包括x。
在实践中,现代JavaScript引擎非常智能。如果它们可以自行证明闭包无法引用x,则可以将其省略。它们这样做的程度会因引擎而异。例如:V8(Chrome等其他地方的引擎)将从堆栈开始使用x、y甚至x引用的对象,然后在退出foo时查看哪些东西仍具有突出的参考,并将这些参考移动到堆中。然后弹出堆栈指针,其他事物就不存在了。:-)
那么,他们如何证明呢?基本上,如果闭包中的代码没有引用它并且没有使用eval或new Function,则JavaScript引擎很可能知道不需要x。
如果你需要确保即使在旧浏览器上,也可以在GC时可用于对象,即使x仍然存在,你也可以这样做:
x = undefined;

这意味着没有任何东西保留了对所使用的对象 x 的引用。因此,即使 x 仍然存在,至少它所引用的对象已经准备好被回收了。这是无害的。但是,现代引擎会为您优化这些内容,除非您面临特定的性能问题并将其追踪到某些在函数返回后不再引用但似乎没有被清理的大型对象分配代码,否则不必担心它。


不幸的是,正如您在下面指出的那样,这种方法也有其限制,比如在这个问题中提到的限制。但不要绝望,接下来在配置文件快照下面可以看到您可以做些什么……

让我们使用Chrome的堆快照功能,在V8中查看这段代码:

function UsedFlagClass_NoFunction() {}
function UnusedFlagClass_NoFunction() {}
function build_NoFunction() {
  var notused = new UnusedFlagClass_NoFunction();
  var used = new UsedFlagClass_NoFunction();
  return function() { return used; };
}

function UsedFlagClass_FuncDecl() {}
function UnusedFlagClass_FuncDecl() {}
function build_FuncDecl() {
  var notused = new UnusedFlagClass_FuncDecl();
  var used = new UsedFlagClass_FuncDecl();
  function unreachable() { notused; }
  return function() { return used; };
}

function UsedFlagClass_FuncExpr() {}
function UnusedFlagClass_FuncExpr() {}
function build_FuncExpr() {
  var notused = new UnusedFlagClass_FuncExpr();
  var used = new UsedFlagClass_FuncExpr();
  var unreachable = function() { notused; };
  return function() { return used; };
}

window.noFunction = build_NoFunction();
window.funcDecl = build_FuncDecl();
window.funcExpr = build_FuncExpr();

下面是扩展的堆快照:

没有可用的描述

在处理build_NoFunction函数时,V8成功地确定了从notused引用的对象无法访问并将其清除,但是在其他两种情况下却没有这样做,尽管unreachable无法访问,因此notused也无法通过它访问。

那么我们该怎么避免这种不必要的内存消耗呢?

对于可以通过静态分析来处理的任何内容,我们可以使用JavaScript到JavaScript编译器(如Google的Closure Compiler)。即使在“简单”模式下,使用Closure Compiler “编译”上述代码的美化结果如下:

function UsedFlagClass_NoFunction() {}
function UnusedFlagClass_NoFunction() {}
function build_NoFunction() {
    new UnusedFlagClass_NoFunction;
    var a = new UsedFlagClass_NoFunction;
    return function () {
        return a
    }
}

function UsedFlagClass_FuncDecl() {}
function UnusedFlagClass_FuncDecl() {}
function build_FuncDecl() {
    new UnusedFlagClass_FuncDecl;
    var a = new UsedFlagClass_FuncDecl;
    return function () {
        return a
    }
}

function UsedFlagClass_FuncExpr() {}
function UnusedFlagClass_FuncExpr() {}
function build_FuncExpr() {
    new UnusedFlagClass_FuncExpr;
    var a = new UsedFlagClass_FuncExpr;
    return function () {
        return a
    }
}
window.noFunction = build_NoFunction();
window.funcDecl = build_FuncDecl();
window.funcExpr = build_FuncExpr();

从上面可以看到,静态分析告诉编译器该代码中的unreachable是无用代码,因此完全将其删除。

但是,在函数执行期间,您可能会使用unreachable,只是在函数完成后不再需要它。它不是无用代码,但是当函数结束时,您不需要它。在这种情况下,您必须采取以下措施:

unused = undefined;

最后,在不再需要该函数时,您可以将其释放:

unused = unreachable = undefined;

(是的,即使它是使用函数声明创建的,你也可以这样做。)
(不过,很遗憾,仅仅这样做是不够的:)
unreachable = undefined;

截至目前(本文撰写时),它不能使V8发现unused可以被清除掉。:(


1
@user3786275:规范是明确无误的。但是实现可以自由优化,只要优化后的代码仍然按照规范运行即可。由于从规范的角度来看,无法判断 x 是否已经被保留,因此删除它的实现完全符合规范(你需要使用内存分析器或类似工具才能确定,这超出了规范的范围)。 - T.J. Crowder
@user3786275:除了V8之外,我不知道还有哪些引擎可以做到这一点。但是SpiderMonkey(Firefox和其他几个地方的引擎)现在非常聪明。JScript在IE9中得到了显着改进,然后在IE10中进一步改进,在IE11中又进一步改进;我不能确定,但我敢打赌他们在其中某个版本开始也这样做了。关于浏览器->引擎映射:Chrome、Chromium和Opera都使用V8;Firefox使用SpiderMonkey,IE使用JScript。我认为Safari使用JSC;我不知道JSC有多先进。IE8很愚蠢而且很慢,所以可能不会这样做。 :-) - T.J. Crowder
@user3786275:在编译器领域,将对象分配到堆栈上,然后在函数调用后将其复制到堆中(如果它们幸存下来)的概念已经有很深的根基。例如,HotSpot(Oracle/Sun的JVM)多年来一直在这样做,以最小化Java垃圾收集问题。因此,在编写编译器的人的世界里,这已经不再是革命性的了。 - T.J. Crowder
1
你的例子确实很有道理,大致上也是我所想的。但这似乎表明Chrome没有做好清理工作:https://dev59.com/KGIj5IYBdhLWcg3w4Izt该漏洞仍然存在,回复似乎是尽管做得更好会更好,但现代浏览器并没有:https://code.google.com/p/chromium/issues/detail?id=315190但你的解释已经足够详细,以至于这个漏洞可能只是旧问题? - user3786275
@user3786275:我要出门了,简单说一下:我对那个案例不起作用有点惊讶,我需要自己证明一下(尽管链接的问题和问题很有说服力),但是V8开发人员直接告诉我,更简单的情况确实有效。就这样吧。(我不明白为什么进一步走会在技术上有任何大的进展。嗯)。非常有趣! - T.J. Crowder
显示剩余7条评论

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