这主要是一个出于好奇的问题。考虑以下功能
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/