javascript - 闭包和作用域在运行时如何在 JavaScript 中表示

标签 javascript garbage-collection closures

这主要是一个出于好奇的问题。考虑以下功能

var closure ;
function f0() {
    var x = new BigObject() ;
    var y = 0 ;
    closure = function(){ return 7; } ;
}
function f1() {
    var x = BigObject() ;
    closure =  (function(y) { return function(){return y++;} ; })(0) ;
}
function f2() {
    var x = BigObject() ;
    var y = 0 ;
    closure = function(){ return y++ ; } ;
}

在任何情况下,函数执行后,(我认为)都无法到达 x 因此 BigObject 可以被垃圾回收,只要因为 x 是对它的最后引用。每当计算函数表达式时,头脑简单的解释器都会捕获整个作用域链。 (一方面,您需要执行此操作才能调用 eval 工作——示例如下)。更智能的实现可能会在 f0 和 f1 中避免这种情况。更智能的实现将允许保留 y,但不保留 x,因为 f2 是有效的。

我的问题是现代 JavaScript 引擎(JaegerMonkey、V8 等)如何处理这些情况?

最后,这里是一个示例,表明即使在嵌套函数中从未提及变量,也可能需要保留它们。

var f = (function(x, y){ return function(str) { return eval(str) ; } } )(4, 5) ;
f("1+2") ; // 3
f("x+y") ; // 9
f("x=6") ;
f("x+y") ; // 11

但是,有一些限制可以防止以编译器可能错过的方式偷偷调用 eval。

最佳答案

并不是说存在阻止您调用静态分析会遗漏的 eval 的限制:只是对 eval 的此类引用在全局范围内运行。请注意,这是 ES3 中 ES5 的一个变化,其中对 eval 的间接和直接引用都在本地范围内运行,因此,我不确定是否有任何东西实际上根据这一事实进行了任何优化。

一个明显的测试方法是让 BigObject 成为一个真正的大对象,并在运行 f0–f2 后强制执行 gc。 (因为,嘿,尽管我认为我知道答案,但测试总是更好!)

所以……

测试

var closure;
function BigObject() {
  var a = '';
  for (var i = 0; i <= 0xFFFF; i++) a += String.fromCharCode(i);
  return new String(a); // Turn this into an actual object
}
function f0() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return 7; };
}
function f1() {
  var x = new BigObject();
  closure =  (function(y) { return function(){return y++;}; })(0);
}
function f2() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return y++; };
}
function f3() {
  var x = new BigObject();
  var y = 0;
  closure = eval("(function(){ return 7; })"); // direct eval
}
function f4() {
  var x = new BigObject();
  var y = 0;
  closure = (1,eval)("(function(){ return 7; })"); // indirect eval (evaluates in global scope)
}
function f5() {
  var x = new BigObject();
  var y = 0;
  closure = (function(){ return eval("(function(){ return 7; })"); })();
}
function f6() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return eval("(function(){ return 7; })"); };
}
function f7() {
  var x = new BigObject();
  var y = 0;
  closure = (function(){ return (1,eval)("(function(){ return 7; })"); })();
}
function f8() {
  var x = new BigObject();
  var y = 0;
  closure = function(){ return (1,eval)("(function(){ return 7; })"); };
}
function f9() {
  var x = new BigObject();
  var y = 0;
  closure = new Function("return 7;"); // creates function in global scope
}

我已经为 eval/Function 添加了测试,看起来这些也是有趣的案例。 f5/f6 之间的区别很有趣,因为 f5 实际上与 f3 完全相同,因为闭包函数实际上是相同的; f6 仅返回已评估的内容,并且由于 eval 尚未评估,编译器无法知道其中没有对 x 的引用。

蜘蛛猴

js> gc();
"before 73728, after 69632, break 01d91000\n"
js> f0();
js> gc(); 
"before 6455296, after 73728, break 01d91000\n"
js> f1(); 
js> gc(); 
"before 6455296, after 77824, break 01d91000\n"
js> f2(); 
js> gc(); 
"before 6455296, after 77824, break 01d91000\n"
js> f3(); 
js> gc(); 
"before 6455296, after 6455296, break 01db1000\n"
js> f4(); 
js> gc(); 
"before 12828672, after 73728, break 01da2000\n"
js> f5(); 
js> gc(); 
"before 6455296, after 6455296, break 01da2000\n"
js> f6(); 
js> gc(); 
"before 12828672, after 6467584, break 01da2000\n"
js> f7(); 
js> gc(); 
"before 12828672, after 73728, break 01da2000\n"
js> f8(); 
js> gc(); 
"before 6455296, after 73728, break 01da2000\n"
js> f9(); 
js> gc(); 
"before 6455296, after 73728, break 01da2000\n"

SpiderMonkey 在除 f3、f5 和 f6 之外的所有内容上都出现 GC“x”。

它看起来尽可能多(即,如果可能,y 和 x),除非在任何仍然存在的函数的作用域链中有直接的 eval 调用。 (即使该函数对象本身已经被 GC 并且不再存在,就像 f5 中的情况一样,这在理论上意味着它可以 GC x/y。)

V8

gsnedders@dolores:~$ v8 --expose-gc --trace_gc --shell foo.js
V8 version 3.0.7
> gc();
Mark-sweep 0.8 -> 0.7 MB, 1 ms.
> f0();
Scavenge 1.7 -> 1.7 MB, 2 ms.
Scavenge 2.4 -> 2.4 MB, 2 ms.
Scavenge 3.9 -> 3.9 MB, 4 ms.
> gc();   
Mark-sweep 5.2 -> 0.7 MB, 3 ms.
> f1();
Scavenge 4.7 -> 4.7 MB, 9 ms.
> gc();
Mark-sweep 5.2 -> 0.7 MB, 3 ms.
> f2();
Scavenge 4.8 -> 4.8 MB, 6 ms.
> gc();
Mark-sweep 5.3 -> 0.8 MB, 3 ms.
> f3();
> gc();
Mark-sweep 5.3 -> 5.2 MB, 17 ms.
> f4();
> gc();
Mark-sweep 9.7 -> 0.7 MB, 5 ms.
> f5();
> gc();
Mark-sweep 5.3 -> 5.2 MB, 12 ms.
> f6();
> gc();
Mark-sweep 9.7 -> 5.2 MB, 14 ms.
> f7();
> gc();
Mark-sweep 9.7 -> 0.7 MB, 5 ms.
> f8();
> gc();
Mark-sweep 5.2 -> 0.7 MB, 2 ms.
> f9();
> gc();
Mark-sweep 5.2 -> 0.7 MB, 2 ms.

除了 f3、f5 和 f6 之外,V8 出现在 GC x 上。这与 SpiderMonkey 相同,请参见上面的分析。 (但是请注意,数字不够详细,无法判断 y 是否在 GC 时 x 没有,我懒得去调查这个。)

卡拉坎

我不想再次运行它,但不用说行为与 SpiderMonkey 和 V8 相同。没有 JS shell 更难测试,但随着时间的推移是可行的。

JSC(硝基)和脉轮

在 Linux 上构建 JSC 很痛苦,而 Chakra 不能在 Linux 上运行。我相信 JSC 与上述引擎具有相同的行为,如果 Chakra 没有,我会感到惊讶。 (做任何更好的事情很快就会变得非常复杂,做任何更糟糕的事情,好吧,你几乎永远不会做 GC 并且有严重的内存问题......)

关于javascript - 闭包和作用域在运行时如何在 JavaScript 中表示,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5368048/

相关文章:

JavaScript 工厂函数修改闭包范围外的变量

Javascript 变量的 "Value"属性未出现在 Intellisense 上

javascript - 使用 AngularJS 加载非常大的图像

java - 垃圾收集和反射

内联代码的 Javascript 垃圾收集

python - 如何找到第一次调用该函数的位置并相应地返回 None ?

javascript - DataTables - 我应该在哪里更新 aData?

javascript - 如何实现在 JavaScript 对象被垃圾回收时触发的函数?

javascript - WebSocket频繁onmessage零拷贝

swift - 按值将 Int 传递给闭包