Skip to content

闭包的实现方式

Livoras edited this page Aug 28, 2020 · 6 revisions

Overview

  • FUNC 指令构建一个新的函数 f1,并且存储定义该函数的 scope 的 closure,定义为 upper closure table (aka UCT)
  • f1 函数在每次被调用的时候都会用 UCT 来构建一个自己的 closure:closure = { ...UCT }, 定义为 current closure table (CCT)
  • VM 中存储一个 heap 数组,来存储所有分配的闭包变量(可能会有内存泄漏,需要设计释放机制)
  • 每次函数调用 ALLOC 指令的时候,会在 heap 上构建一个新的位置,并且在 CCT 上记录该变量在 heap 上的位置
...                    
VAR @c33               
ALLOC @c33 -> [33]  ---.                            Code
...                    |
----------------------------------------------
vm: {                  |
  heap: [-, -, -, -, {@c0}]                         Virtual Machine
}                      |  
----------------------------------------------
                       |
CCT: { 33: 4 }     <---.                            Current Closure Table (stored in vm)

同一个函数被多次调用的时候,虽然说它们的 @c33 的 key 都是 33,但是在 heap 在位置不一样,并且记录在 CCT 上。

函数闭包的传入

函数在运行的时候,可能遇到 FUNC 的指令。

FUNC add @@f23

FUNC 会根据 @@f23 函数的原型(funcInfo,存储了函数的起始地址、参数数量等)构建一个新的 JavaScript 函数(Wrappered VM Function, aka, WVF),并且存储到 add 寄存器当中。构建这 WVF 的时候,会给它一个 closureTable(UCT),并且存储在闭包变量当中(WVF 通过函数构建)。

当 WVF 被调用的时候 CALL_REG add 0 false,WVF 会通过 UCT 构建 CCT,并且存储到 vm 当中。后续遇到闭包变量的时候(CLOSURE REGISTER)的时候可以在这个 CCT 上查询到对应在 heap 上的位置并且获取到闭包变量的值。

可以观察到,每个 CCT 都是由 UCT 构建,遇到 ALLOC 的时候,闭包变量在 heap 上分配并且记录到 CCT 当中。在遇到 FUNC 的时候,CCT 会传入给下一个函数作为下一个函数的 UCT。这样就构建起了一个闭包记录的链条。并且不需要追溯,因为链条所有闭包数据都记录在 CCT 当中 (这样导致的问题可能是,函数 CCT 会被复制所有它以上所有层级的的闭包变量数据到 CCT 当中,即使它自己没有用到;如果函数嵌套很多层,可能会有浪费内存空间的嫌疑)。

块级作用域的闭包变量

对于函数作用域的闭包变量用上面的方式是没有问题的,用 ES5 var 声明的变量都属于函数,一个函数一个闭包变量 CCT 已经能够满足需求。但是对于 ES6 的块级作用域,就有点难受了:

const foo = (c) => {          // CCT
  let a = 1
  let b = 2
  let c = -1
  let d = -2
  console.log(a, b)
  if (c) {                    // start block
    const a = 3
    const b = 4
    return () => a + b
  } else {                    // end block & start block
    const c = 5
    const d = 6
    return () => c - d 
  }                           // end block
  console.log(a, b, c, d)
}

const bar = foo(true)

普通变量在运行时和闭包变量的实现方式是分离的,在考虑如何实现闭包变量的时候不需要考虑普通变量的实现方式。

这里几个操作:

  1. front
  2. back
  3. fork
export class Scope {
  constructor(public scope: any = {}) {
  }

  public front(): void {
    this.scope = Object.setPrototypeOf({}, this.scope)
  }

  public back(): void {
    this.scope = Object.getPrototypeOf(this.scope)
  }

  public fork(): Scope {
    return new Scope(this.scope)
  }

  public set(key: any, value: any): void {
    this.scope[key] = value
  }

  public get(key: any): any {
    return this.scope[key]
  }
}

有这几个从操作就可以控制块级作用域和函数作用域的闭包分配。