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

在JavaScript中如何在运行时表示闭包和作用域,javascript,garbage-collection,closures,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) ; } f

这主要是出于好奇。考虑以下函数

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,因此只要x是对它的最后一个引用,BigObject就可以被垃圾收集。只要对函数表达式求值,头脑简单的解释器就会捕获整个作用域链。(首先,您需要这样做才能调用eval work——下面的示例)。更智能的实现可能会在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。

在正常情况下,函数中的局部变量是在堆栈上分配的——当函数返回时,它们“自动”消失。我相信许多流行的JavaScript引擎都在堆栈机器体系结构上运行解释器(或JIT编译器),因此这种方法应该是合理有效的

现在,如果一个变量在闭包中被引用(即通过一个本地定义的函数,该函数稍后可能会被调用),那么“内部”函数将被分配一个“范围链”,该范围链从函数本身的最内部范围开始。下一个作用域是外部函数(包含访问的局部变量)。解释器(或编译器)将创建一个“闭包”,本质上是在堆(而不是堆栈)上分配的一段内存,其中包含作用域中的那些变量

因此,如果在闭包中引用局部变量,它们将不再在堆栈上分配(这将使它们在函数返回时消失)。它们的分配方式与普通的、长寿命的变量一样,“作用域”包含指向每个变量的指针。内部函数的“作用域链”包含指向所有这些“作用域”的指针

一些引擎通过省略隐藏的变量(即内部作用域中的局部变量所覆盖的变量)来优化作用域链,因此在您的情况下,只要变量“x”仅在内部作用域中访问,并且在外部作用域中没有“eval”调用,就只剩下一个BigObject。一些引擎“展平”作用域链(我认为V8可以做到这一点)以实现快速变量解析——只有在两个引擎之间没有“eval”调用(或者没有对可能进行隐式eval的函数的调用,例如setTimeout)的情况下才能做到这一点


我想请一些JavaScript引擎大师提供比我能提供的更多有趣的细节。

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

测试这一点的一个显而易见的方法是使BigObject成为真正的大对象,并在运行f0–f2之后强制执行gc。(因为,嘿,我想我知道答案,测试总是更好!)

所以

测试 SpiderMonkey在除f3、f5和f6之外的所有对象上都显示为GC“x”

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

V8 V8在除f3、f5和f6之外的所有对象上都显示为GC x。这与SpiderMonkey相同,请参见上面的分析。(但是请注意,这些数字不够详细,无法判断y是否为GC'd,而x不是,我没有费心调查这一点。)

卡拉坎 我不会再费心运行它了,但不用说它的行为和SpiderMonkey和V8是一样的。没有JS外壳很难测试,但随着时间的推移是可行的

JSC(硝基)和脉轮
在Linux上构建JSC是一件痛苦的事情,而Chakra不能在Linux上运行。我相信JSC对上述引擎有同样的行为,如果查克拉没有,我会感到惊讶。(做得更好很快就会变得非常复杂,做得更差,好吧,你几乎永远不会做GC并且会有严重的内存问题……

我会在稍后发布一个更完整的答案,但是:SpiderMonkey是唯一一个仍然基于堆栈的主要JS引擎;所有其他主要的JS引擎(即Chakra、JSC、V8和Carakan)都是基于注册的。真的吗?请张贴你的答案。这很有趣!此外,请查看是否可以详细说明对范围链所做的优化。谢谢可惜我只能投一次票。我会给这个实验更多的投票。顺便说一句,有没有可能在猴子身上做实验?我知道它会做进一步的优化,但为了正确起见,不太可能优化掉f3、f5和f6。@Stephen:这实际上是用JägerMonkey完成的。JIT行为根本不影响GC。在
f7
f8
和f9中的本地变量的GC是一个bug吗?可能是重复的
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
}
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"
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.