JavaScript在运行时如何表示闭包和范围

这大部分是出于好奇的问题。 考虑以下function

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是最后一个引用。 一个简单的解释器会捕捉整个范围链,每当一个函数expression式被评估。 (一方面,您需要这样做来打电话给eval工作 – 下面的例子)。 更聪明的实现可能会在f0和f1中避免这种情况。 一个更聪明的实现将允许y被保留,但不是x ,因为f2是有效的。

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

最后,这里是一个例子,表明即使在嵌套函数中从不提及variables也可能需要保留。

 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的间接和直接引用都在本地范围内运行,因此,我不确定是否有任何实际上基于此事实进行了任何优化。

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

所以…

考试

 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的testing,看起来这些也是有趣的情况。 f5 / f6之间的区别很有意思,因为f5实际上和f3是一样的,因为闭包的function是相同的。 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" 

除f3,f5和f6外,SpiderMonkey在GC上都显示为“x”。

它似乎尽可能地(即,如果可能的话,y,以及x)除非在任何仍然存在的函数的范围链中存在直接的eval调用。 (即使这个函数对象本身已经GC'd并且不再存在,就像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'd,我没有打算去调查这个)。

的Carakan

我不打算再次跑这个,但不用说,行为是相同的SpiderMonkey和V8。 更难以testing没有JSshell,但随着时间的推移。

JSC(硝基)和查克拉

构buildJSC在Linux上是一种痛苦,Chakra不能在Linux上运行。 我相信JSC对上述引擎也有同样的行为,如果Chakra没有,我会感到惊讶。 (做得更好很快就变得很复杂,做得更糟糕,好吧,你几乎不会做GC,并且有严重的内存问题…)

在正常情况下,函数中的局部variables被分配到堆栈上,并且在函数返回时它们会自动地消失。 我相信许多stream行的JavaScript引擎在堆栈机器体系结构上运行解释器(或JIT编译器),所以这种观察应该是合理有效的。

现在,如果在闭包中引用一个variables(即通过本地定义的函数稍后可能会被调用),则“内部”函数将被赋予一个“范围链”,该范围链从作为函数本身的最内层范围 。 下一个作用域是外部函数(包含访问的局部variables)。 解释器(或编译器)将创build一个“闭包”,本质上是在堆中分配的一块内存(不是堆栈),它包含范围中的这些variables。

因此,如果局部variables在闭包中被引用,那么它们不再被分配到堆栈上(当函数返回时,这些variables将会消失)。 它们像正常的,长期存在的variables一样被分配,“范围”包含一个指向它们的指针。 内部函数的“范围链”包含指向所有这些“范围”的指针。

一些引擎通过忽略被隐藏的variables(即被内部作用域中的局部variables覆盖)来优化作用域链,所以在你的情况下,只有一个BigObject被保留,只要variables“x”只在内部作用域中被访问,并且在外部范围内没有“eval”调用。 有些引擎将范围链(我认为V8就是这样做的)用于快速variables分析 – 只有在中间没有“eval”调用时才能完成(或者不需要调用可能会执行隐式eval的函数,例如的setTimeout)。

我会邀请一些JavaScript引擎大师提供更多的细节比我可以。