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

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

源码仓库

1. 本文目标

本篇实现闭包:

javascript 复制代码
function makeAdder(x) {
  return function add(y) {
    return x + y;
  };
}

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

核心问题:

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

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

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

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

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

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

这把钥匙就是:

plaintext 复制代码
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

它解决什么问题

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

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

Scope Chain

它解决什么问题

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

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

BindingRef

它解决什么问题

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

教学版简化实现
javascript 复制代码
{ 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. 伪代码

plaintext 复制代码
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. 教学版实现代码

javascript 复制代码
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. 示例输入与输出

javascript 复制代码
function outer() {
  let x = 1;
  return function inner() {
    return x;
  };
}

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

简化 IR:

plaintext 复制代码
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 沿环境链向外走。

下一篇将继续讲:

plaintext 复制代码
第 10 篇:对象、数组与属性访问
相关推荐
晴天161 小时前
前端 SSR、BFF 原理介绍
前端·javascript·网络·html
全栈技术负责人2 小时前
AI 效能工具建设:Agent 可观测性技能技术方案与实践
前端·ai·ai编程
梦想平凡2 小时前
百游棋牌源代码开发搭建教程(一):全端工程结构、开发环境配置与首次启动验证
前端·javascript·源代码管理
妙码生花2 小时前
两个月 59 篇 AI 开发日志 + Golang 商业级实战项目收工后,得来的 AI 使用心法-上
前端·后端·gin
vx-Biye_Design2 小时前
springboot中国传统节日宣传平台49078-计算机课程设计、毕业设计
java·前端·vue.js·spring boot·后端·课程设计·idea
妙码生花2 小时前
两个月 59 篇 AI 开发日志 + Golang 商业级实战项目收工后,得来的 AI 使用心法-下
前端·后端·go
Highcharts2 小时前
常见报错排雷指南3:兼容性问题的官方解法
javascript·数据可视化
5335ld2 小时前
app版本更新(vue3+unibest+静默更新+强制更新)
开发语言·javascript·ecmascript
传奇开心果编程3 小时前
【xilem0.4基础语法学与练】第22课:on_click / on_change 与手势系统的实战
学习·rust·前端框架