«
V8 closure(闭包)

时间:2022-6   


1、变量不是闭包。

在v8中,关于closure的定义如下:

JSFunctions are pairs (context, function code), sometimes also called closures.

一个JSFunction对象称为closure,这个JSFunction对象包含context和function code,除此之外,还包含sharedfunctioninfo、feedback、header……

是的,如题主推测那样,v8就是那么“简单粗暴”,每个函数的运行都需要创建闭包,而闭包也是一段js代码在v8中运行的基础,必不可少。

不用作用域链,是因为每次执行还要到作用域中一层层查找,效率太低,编译后直接通过地址取变量,没有查找过程

利用题主的代码:

关于推断时机很简单,就是通过引用,也就是指针。如果父子关系的func,父内部不在外部做任何内部指针引用,那么执行结束,就只能被gc回收。也就不会有所谓的闭包一说。

另外就是作用域是作用域,闭包是闭包两者不一样,虽然都是对状态的维护,但两者出现的实际不一样。

const otherFun2 = () => {
  const whetherClosure = 2
  const fun2 = () => {
    console.log(whetherClosure + 1)
  }
  fun2()
}

const otherFun2Return = otherFun2()

在v8中产生的字节码如下,我们可以看到,在每个子函数调用之前,都会在父函数中执行createclosure字节码创建闭包,而在执行createclosure之前又会执行createfunctioncontext字节码创建函数执行上下文,如果没有执行createfunctioncontext则说明父函数中没有活动变量,即子函数与父函数共用一个上下文。

](http://pengwu.ink/content/uploadfile/202206/7f0c1e0c667f2107dd6de0d40c07eed7.jpg)**2、关于调用链问题**

这个问题只需要关注context即可,一段js想在v8中运行,首先创建context,然后在v8中注册成JSFunction对象(closure)。

ecma规范中对于context定义如下:

An execution context contains whatever implementation specific state is necessary to track the execution progress of its associated code.[3]

正在运行的执行上下文始终位于栈顶,每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就会创建一个新的执行上下文。新创建的上下文被压入栈中,成为当前执行的上下文current_context。

在v8中,所有的上下文都包含三部分:scope_info、previous、extension(v8/src/objects/contexts.h)。

1) scope_info:描述当前上下文的范围信息(包含静态分配的context slots名称和堆栈分配的局部变量)。

2) previous:指向前一个context的指针。

3) extension:附加数据,对于不同的上下文(native context、module context等)包含的内容不一样。

显而易见,子函数访问父函数的成员是通过子函数context中的previous指针去访问父函数的context实现的。

而function context,是JSFunction对象(closure)的成员。

首先,whetherClosure是闭包,感觉你对闭包的定义还不甚清晰,可以看下闭包的MDN解释

然后,解释问题
1.完全可以通过作用域链去访问父级变量,为什么多此一举要闭包?可以根据闭包的定义

](http://pengwu.ink/content/uploadfile/202206/b36a0c20852236403869bd09804f2dbb.jpg)),天然情况下就是正常的词法作用域(你所指的作用域链查找)。只是当你return了内部函数之后,由于内部函数对外部函数的变量的引用存在,因此这种引用捆绑的关系被打包得以在外层函数外继续使用罢了
2.v8怎么推断子函数在运行时是否会告诉外部,v8并不推断,只是一如既往地--标记清除和引用计数(实际更复杂,包含主,副GC,stop-the-world和scavenge两种并用)。在问题2的demo中,registerToWindow拿到了demo内常量returnFun的引用,如果registerToWindow中又有类似window.onXXX = returnFun的副作用,那么整个returnFun的闭包(demo内和returnFun捆绑的上下文)不会被GC;如果没有副作用,那么returnFun的闭包就会被销毁。

现在的编译器和虚拟机暂时还不支持“let”关键字申明语句(后期会加入),所以先用“var”来实现题主例子。。。题主例子很简单,就用这个把

首先明确几点:

1、GC只会去释放堆中分配了内存的对象(string,json,array等)。像var count = 0,这种(虽然JavaScript管这也叫做对象)其实底层并不是按照对象来处理(至少这里的JavaScript引擎不是这么实现的),包括浮点数等,GC并不会去处理(标记)这种类型。

2、匿名函数,包括箭头函数,其实就是函数申明,只是按照表达式来处理,可以看作伪装成表达式的函数申明

3、基于第2点,函数申明在另一个函数内部,编译器可以很明确的知道,申明在父函数内部的子函数用到了某个父函数(包裹在函数外部的函数为父函数)的变量地址,注意:我说的是变量地址,甚至子函数在去嵌套子函数,嵌套n层,编译器都能建立子函数使用父函数变量地址的对应关系

4、基于第3点,既然父子函数变量使用关系(并非引用关系)都明确了,接下给闭包函数单独开辟一块内存,并按照对应关系,把要使用的变量从父函数中拷贝过来就行了,GC会对这个单独的内存进行标记,所以就算是:string,json,array等对象并不会被GC释放掉,是安全的。。。

为什么要把变量拷贝过来,为不是引用过来,主要是考虑异步闭包函数的执行,异步闭包函数执行时,父函数的栈早就回收另作他用了,所以引用父函数的变量肯定会造成问题,甚至程序崩溃

好了有了这些理论基础,我来大致捋一下底层实现原理(主要是字节码实现原理,编译器和父子函数建立对应关系等内容太多了,我就不展开),虚拟机和编译器都用纯C语言实现,其实主要看生成的字节码就可以。

这段代码编译生成的字节码并不多,直接贴出来:

编译器会创建中文版的调试文件,还是看得比较清楚,按照"test"函数和"TIM200000000CLOSURE"匿名函数(编译器会分匹配一个名称)的字节码执行流程就能看明白。

注意:test函数执行并不会返回一个新的匿名函数,test返回的其实是匿名函数的地址(也就是0)。

按照test字节码执行流程主要是C语言实现:

1、“设置闭包环境”指令实现:

static TIM_PUBLIC(TMVM_Value *)
get_value_from_current_stack(TMVM_VirtualMachine *tvm, Stack *stack, int32 func_idx, int32 var_idx)
{
    int32 i = stack->stack_pointer - 1;
    CallInfo *ci;

    for (;;) {
        for (; i>=0; i--) {
            if (stack->pointer_flags[i] == TMVM_IS_CALL_INFO) {
                break;
            }
        }
        if (i < 0) {
            break;
        }
        i -= CALL_INFO_ALIGN_SIZE_START;
        ci = (CallInfo*)&amp;stack->stack[i];
        if (ci->self_function->kind == TIMVM_FUNCTION &amp;&amp; ci->self_function->u.tim_f.index == func_idx) {
            if (var_idx >= tvm->executable->function[func_idx].parameter_count) {
                return &amp;stack->stack[i + var_idx + 1 - tvm->executable->function[func_idx].parameter_count + CALL_INFO_ALIGN_SIZE_START];
            }
            int32 argc = stack->stack[i - 2].u.int_value;
            return &amp;stack->stack[i - argc - 2 + var_idx];
        }
        i--;
    }

    return NULL;
}

static TIM_PUBLIC(void)
set_closure_env(TMVM_VirtualMachine *tvm, HeapStack *hs, uint16 func_idx)
{
    ClosureUnit *cu = tvm->executable->function[func_idx].closure_var;
    int32 count = tvm->executable->function[func_idx].closure_var_count;
    int32 i = 0;
    TMVM_Value *val;
    FunctionClosureEnv *cvars = get_clousre_env(tvm, hs, func_idx);

    for (; i<count; i++) {
        val = get_value_from_current_stack(tvm, &amp;hs->stack, cu[i].func_idx, cu[i].parent_var_index);
        if (val) {
            cvars->values[i] = *val;
            cvars->value_done_flags[i] = TMVM_TRUE; //set_child_clousre_env 时赋值为false,为假时set_parent_closure_env将不会去设置父函数,因为父函数已经不在栈上了
        }
    }
}

2、“设置子闭包环境”指令实现:

static TIM_PUBLIC(void)
set_value_from_current_function_stack(TMVM_VirtualMachine *tvm, Stack *stack, TMVM_Value *val, int32 val_idx, int32 func_idx)
{
    int32 i = stack->stack_pointer;
    CallInfo *ci;

    for (;;) {
        for (; i>=0; i--) {
            if (stack->pointer_flags[i] == TMVM_IS_CALL_INFO) {
                break;
            }
        }
        if (i < 0) {
            break;
        }
        i -= CALL_INFO_ALIGN_SIZE_START;
        ci = (CallInfo*)&amp;stack->stack[i];
        if (ci->self_function->kind == TIMVM_FUNCTION &amp;&amp; ci->self_function->u.tim_f.index == func_idx) {
            if (val_idx >= tvm->executable->function[func_idx].parameter_count) {
                *val = stack->stack[i + val_idx + 1 - tvm->executable->function[func_idx].parameter_count + CALL_INFO_ALIGN_SIZE_START];
            } else {
                int32 argc = stack->stack[i - 2].u.int_value;
                *val = stack->stack[i - argc - 2 + val_idx];
            }
        }
        i--;
    }
}

static TIM_PUBLIC(void)
set_child_closure_env(TMVM_VirtualMachine *tvm, HeapStack *hs, uint16 func_idx)
{
    ClosureUnit *cu = tvm->executable->function[func_idx].children_closure_var;
    int32 count = tvm->executable->function[func_idx].children_closure_var_count;
    int32 i = 0;
    FunctionClosureEnv *cvar;

    for (; i<count; i++) {
        cvar = get_clousre_env(tvm, hs, cu[i].func_idx); //找出子函数的值
        set_value_from_current_function_stack(tvm, &amp;hs->stack, &amp;cvar->values[cu[i].child_var_index], cu[i].parent_var_index, func_idx);
        cvar->value_done_flags[cu[i].child_var_index] = TMVM_FALSE;
    }
}

当然看这些代码片段意义不大,这里只是提供参考一下吧