手搓JSVM第 9 篇:闭包:函数如何记住外部变量

0 阅读3分钟

第 9 篇:闭包:函数如何记住外部变量

闭包封面:函数代码加创建时的环境

源码仓库

1. 本文目标

本篇实现闭包:

function makeAdder(x) {
  return function add(y) {
    return x + y;
  };
}

const add1 = makeAdder(1);
add1(2); // 3

核心问题:

内层函数执行时,为什么还能读取外层函数已经创建过的变量 x

2. 为什么闭包需要环境捕获

按直觉,外层函数返回后,它的局部变量就该消失。但闭包要求这些变量继续可见。

形象化比喻:带走房间钥匙

外层函数像一间房,里面有储物柜 x。内层函数创建时,带走了这间房的钥匙。

外层函数结束后,这间房不会被拆掉,因为还有函数拿着它的钥匙。

这把钥匙就是:

parentEnv

带走房间钥匙:内层函数拿着 parentEnv,房间不会被拆

3. 前置知识

概念说明
Closure函数 + 捕获环境
Scope Chain多个 env 通过 parent 串起来
Depth从当前 env 向外走几层
Captured Environment函数创建时保存的环境引用

4. 源码中的对应位置

概念源码位置说明
ScopeFrame.parentsrc/compiler/lowering.ts编译期作用域链
resolve()src/compiler/lowering.ts计算变量所在 depth 和 slot
createClosure()src/compiler/runtime-gen.ts创建捕获 parentEnv 的函数
resolveEnv()src/compiler/runtime-gen.ts运行时按 depth 找外层 env
MAKE_FUNCTIONsrc/runtime/opcodes.ts创建函数对象

5. 核心数据结构

Closure

它解决什么问题

把函数代码和它创建时所在的环境绑在一起。

教学版简化实现
function createClosure(functionId, parentEnv) {
  return { type: 'closure', functionId, parentEnv };
}

Scope Chain

它解决什么问题

让变量查找可以从内层作用域一层层走到外层作用域。

教学版简化实现
function resolveEnv(env, depth) {
  let current = env;
  while (depth > 0) {
    current = current.parent;
    depth--;
  }
  return current;
}

作用域链:放大镜从内层环境向外一层,找到 x

BindingRef

它解决什么问题

编译期把变量名解析成 { depth, slot },运行时就不用再按名字查找。

教学版简化实现
{ kind: 'slot', depth: 1, slot: 0 }

变量名在编译期解析成层数和格子号

6. Mermaid 图解

flowchart LR
    A[&#34;Global Env&#34;] --> B[&#34;makeAdder Env<br/>x = 1&#34;]
    B --> C[&#34;add Env<br/>y = 2&#34;]
    C -->|depth 1| B
flowchart TD
    A[&#34;创建 inner function&#34;] --> B[&#34;保存 parentEnv&#34;]
    B --> C[&#34;外层函数 return&#34;]
    C --> D[&#34;inner 之后被调用&#34;]
    D --> E[&#34;通过 parentEnv 读取 x&#34;]

7. 伪代码

MAKE_FUNCTION functionId:
    closure = createClosure(functionId, currentEnv)
    regs[dst] = closure

LOAD_SLOT dst, depth, slot:
    targetEnv = resolveEnv(currentEnv, depth)
    regs[dst] = targetEnv.values[slot]

8. 教学版实现代码

function createClosure(functionId, parentEnv) {
  return { type: 'closure', functionId, parentEnv };
}

function resolveEnv(env, depth) {
  let current = env;

  while (depth > 0) {
    current = current.parent;
    depth--;
  }

  return current;
}

function readSlot(env, depth, slot) {
  const target = resolveEnv(env, depth);
  return target.values[slot];
}

9. 示例输入与输出

function outer() {
  let x = 1;
  return function inner() {
    return x;
  };
}

const fn = outer();
fn(); // 1

简化 IR:

outer:
load_const r0, 1
init_slot slot0(x), r0
make_function r1, inner, parentEnv=current
return r1

inner:
load_slot r0, depth=1, slot0(x)
return r0

10. 执行过程拆解

Step动作环境
1调用 outer创建 outer env
2初始化 xouter env: x=1
3创建 innerinner 捕获 outer env
4outer 返回 innerouter env 仍被引用
5调用 inner创建 inner env,parent 指向 outer env
6读取 xdepth=1 找到 outer env

11. 与原始源码的差异

主题教学版正式源码
Closure普通对象createClosure() 返回 JS function
env 链单纯 parent还携带 this、args、slotKinds
depth手写编译期 resolve() 计算
函数种类普通函数支持 async/generator 等

12. 常见问题

Q1:闭包捕获的是值还是变量?

捕获的是环境引用,不是当时的值的拷贝。因此如果外层变量之后被修改,闭包读取到的是修改后的值。

Q2:外层函数结束后 env 为什么还存在?

因为闭包仍然引用它,垃圾回收不会释放仍可达的对象。

外层函数已返回,环境因闭包仍引用而继续存活

13. 本文小结

闭包 = 函数代码 + 创建时的环境。VM 层面只需要两件事:MAKE_FUNCTION 时存下 parentEnv,读变量时按 depth 沿环境链向外走。

下一篇将继续讲:

第 10 篇:对象、数组与属性访问