1. 本文目标
本篇实现闭包:
<span>function</span> <span>makeAdder</span>(<span>x</span>) {
<span>return</span> <span>function</span> <span>add</span>(<span>y</span>) {
<span>return</span> x + y;
};
}
<span>const</span> add1 = <span>makeAdder</span>(<span>1</span>);
<span>add1</span>(<span>2</span>); <span>// 3</span>
核心问题:
内层函数执行时,为什么还能读取外层函数已经创建过的变量
x?
2. 为什么闭包需要环境捕获
按直觉,外层函数返回后,它的局部变量就该消失。但闭包要求这些变量继续可见。
形象化比喻:带走房间钥匙
外层函数像一间房,里面有储物柜 x。内层函数创建时,带走了这间房的钥匙。
外层函数结束后,这间房不会被拆掉,因为还有函数拿着它的钥匙。
这把钥匙就是:
parentEnv
3. 前置知识
| 概念 | 说明 |
|---|---|
| Closure | 函数 + 捕获环境 |
| Scope Chain | 多个 env 通过 parent 串起来 |
| Depth | 从当前 env 向外走几层 |
| Captured Environment | 函数创建时保存的环境引用 |
4. 源码中的对应位置
| 概念 | 源码位置 | 说明 |
|---|---|---|
| `ScopeFrame.parent` | `src/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_FUNCTION` | `src/runtime/opcodes.ts` | 创建函数对象 |
5. 核心数据结构
Closure
它解决什么问题
把函数代码和它创建时所在的环境绑在一起。
教学版简化实现
<span>function</span> <span>createClosure</span>(<span>functionId, parentEnv</span>) {
<span>return</span> { <span>type</span>: <span>'closure'</span>, functionId, parentEnv };
}
Scope Chain
它解决什么问题
让变量查找可以从内层作用域一层层走到外层作用域。
教学版简化实现
<span>function</span> <span>resolveEnv</span>(<span>env, depth</span>) {
<span>let</span> current = env;
<span>while</span> (depth > <span>0</span>) {
current = current.<span>parent</span>;
depth--;
}
<span>return</span> current;
}
BindingRef
它解决什么问题
编译期把变量名解析成 { depth, slot },运行时就不用再按名字查找。
教学版简化实现
{ <span>kind</span>: <span>'slot'</span>, <span>depth</span>: <span>1</span>, <span>slot</span>: <span>0</span> }
6. Mermaid 图解
flowchart LR
A["Global Env"] --> B["makeAdder Env<br/>x = 1"]
B --> C["add Env<br/>y = 2"]
C -->|depth 1| B
flowchart TD
A["创建 inner function"] --> B["保存 parentEnv"]
B --> C["外层函数 return"]
C --> D["inner 之后被调用"]
D --> E["通过 parentEnv 读取 x"]
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. 教学版实现代码
<span>function</span> <span>createClosure</span>(<span>functionId, parentEnv</span>) {
<span>return</span> { <span>type</span>: <span>'closure'</span>, functionId, parentEnv };
}
<span>function</span> <span>resolveEnv</span>(<span>env, depth</span>) {
<span>let</span> current = env;
<span>while</span> (depth > <span>0</span>) {
current = current.<span>parent</span>;
depth--;
}
<span>return</span> current;
}
<span>function</span> <span>readSlot</span>(<span>env, depth, slot</span>) {
<span>const</span> target = <span>resolveEnv</span>(env, depth);
<span>return</span> target.<span>values</span>[slot];
}
9. 示例输入与输出
<span>function</span> <span>outer</span>(<span></span>) {
<span>let</span> x = <span>1</span>;
<span>return</span> <span>function</span> <span>inner</span>(<span></span>) {
<span>return</span> x;
};
}
<span>const</span> fn = <span>outer</span>();
<span>fn</span>(); <span>// 1</span>
简化 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 | 初始化 x | outer env: `x=1` |
| 3 | 创建 inner | inner 捕获 outer env |
| 4 | outer 返回 inner | outer env 仍被引用 |
| 5 | 调用 inner | 创建 inner env,parent 指向 outer env |
| 6 | 读取 x | depth=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 篇:对象、数组与属性访问
适合正在实现解释器或编译器的读者。「房间钥匙」的比喻加上depth+slot解析路径,把闭包讲得可落地;同时点明捕获的是环境引用而非值拷贝这一常见坑。