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

时间:2011-03-20 11:03:25

标签: 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调用。

2 个答案:

答案 0 :(得分:36)

有些限制阻止你调用静态分析会遗漏的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的引用。

SpiderMonkey的

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”。

除非在任何仍然存在的函数的范围链内存在直接eval调用,否则它似乎尽可能地(即,在可能的情况下,y和x)。 (即使该函数对象本身已经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相同,请参阅上面的分析。 (但请注意,当x不是时,数字不够详细,无法判断y是否正在进行GC测试,我不打算对此进行调查。)

的Carakan

我不打算再打扰这个,但不用说行为与SpiderMonkey和V8相同。没有JS shell就更难测试,但随着时间的推移可以测试。

JSC(Nitro)和Chakra

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

答案 1 :(得分:10)

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

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

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

某些引擎通过省略被遮蔽的变量(即由内部作用域中的局部变量覆盖)来优化作用域链,因此在您的情况下只剩下一个BigObject,只要变量“x”仅在内部作用域,外部作用域中没有“eval”调用。一些引擎“展平”范围链(我认为V8会这样做)以实现快速可变分辨率 - 只有在中间没有“eval”调用时才能完成(或者不调用可能执行隐式eval的函数,例如的setTimeout)。

我邀请一些JavaScript引擎专家提供比我更多的细节。