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

文章来源声明: 原文作者:泯泷; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684212531368575002; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合正在实现解释器或编译器的读者。「房间钥匙」的比喻加上depth+slot解析路径,把闭包讲得可落地;同时点明捕获的是环境引用而非值拷贝这一常见坑。

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

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

源码仓库

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

带走房间钥匙:内层函数拿着 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;
}

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

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[&#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. 教学版实现代码

<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初始化 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 篇:对象、数组与属性访问