了解 javascript v8 中的闭包变量捕获

Understanding javascript closure variable capture in v8

我理解这样的语义,即闭包持有对变量的引用会延长它的生命周期,使原始变量不受调用堆栈的限制,因此应该对闭包捕获的那些变量进行特殊处理。

我也理解同一范围内的变量可能会被不同地对待,这取决于它是否被当今 javascript 引擎中的闭包捕获。例如,

function foo(){
    var a=2;
    var b=new Array(a_very_big_number).join('+');
    return function(){
        console.log(a);
    };
}
var b=foo();

因为没有人在 foo 中持有对 b 的引用,所以不需要在内存中保留 b,因此使用的内存可以在 [=14= 后立即释放] returns(甚至从未在进一步优化下创建)。

我的问题是,为什么 v8 似乎在每个调用上下文中将所有闭包引用的所有变量打包在一起?例如,

function foo(){
    var a=0,b=1,c=2;
    var zig=function(){
        console.log(a);
    };
    var zag=function(){
        console.log(b);
    };
    return [zig,zag];
}

zigzag 似乎都引用了 ab,即使很明显 bzig。当 b 非常大并且 zig 持续很长时间时,这可能会很糟糕。

但是站在实现的角度,我无法理解为什么这是必须的。据我所知,不用调用eval,在执行前就可以确定作用域链,从而确定引用关系。引擎应该意识到,当 zig 不再可用时,a 也不再可用,因此引擎将其标记为垃圾。

chrome 和 firefox 似乎都遵守规则。标准是否规定任何实施都必须这样做?或者这个实现更实用、更高效?我很纳闷。

该标准并未提及任何关于垃圾回收的内容,但给出了一些应该发生的事情的线索。 参考:Standard

An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a Function Declaration contains two nested Function Declarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current execution of the surrounding function."

Section 13 Function definition
  step 4: "Let closure be the result of creating a new Function object as specified in 13.2"

Section 13.2 "a Lexical Environment specified by Scope" (scope = closure)

Section 10.2 Lexical Environments:
"The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.

因此,函数将可以访问父级的环境。

主要障碍是可变性。如果两个闭包共享相同的 var 那么它们必须以一种方式这样做,即从一个闭包中改变它在另一个闭包中是可见的。因此,不可能将引用变量的值 复制 到每个闭包环境中,就像函数式语言所做的那样(其中绑定是不可变的)。您需要共享指向公共可变堆位置的指针。

现在,您可以将每个捕获的变量分配为堆上的一个单独单元格,而不是一个数组容纳所有变量。但是,这在 space 和时间上通常会更昂贵,因为您需要多次分配和两个间接级别(每个闭包指向其自己的闭包环境,后者指向每个共享的可变变量单元格)。对于当前的实现,它只是每个作用域的一个分配和一个访问变量的间接访问(单个作用域内的所有闭包都指向同一个可变变量数组)。缺点是某些使用寿命比您预期的要长。这是一个权衡。

其他考虑因素是实现的复杂性和可调试性。由于 eval 等可疑功能以及调试器可以检查作用域链的期望,基于作用域的实现更易于处理。