闭包:函数记住的不只是代码
目标
闭包常被说得玄乎。本节课用上一节已经写好的解释器,通过一个"计数器"例子把闭包讲透:函数记住的是它定义时的那个环境(引用),而不是调用时的。下面给出完整可运行程序。
运行环境:Node.js v18+。运行方式:保存为 closure.js,执行 node closure.js(复用 l6 的完整解释器,仅改调用示例)。
// closure.js —— 复用 l6 的完整解释器(tokenize/parse/Environment/ev/run 同上)
// 此处只展示"调用示例",其余函数与 func.js 完全一致
function run(src) {
const env = new Environment()
env.define('print', (...xs) => console.log(...xs))
for (const s of parse(src)) ev(s, env)
}
// ===== 调用示例:返回一个"记住了 c"的函数 =====
run(`
func makeCounter() {
var c = 0;
return func() {
c = c + 1;
return c;
};
}
var next = makeCounter();
print(next()); // 1
print(next()); // 2
print(next()); // 3
`)为什么三次 next() 在"累加"?因为 makeCounter 返回的那个匿名函数,其 closure 指向 makeCounter 调用时创建的那个 Environment(里面存着 c=0)。每次 next() 调用,开的新 fnEnv 的父作用域就是这个"带 c 的环境",c = c + 1 沿链找到并改写它。所以 c 一直活着、一直在涨。
(完整解释器代码与 func.js 相同;为节省篇幅此处只列调用示例,运行时请把 func.js 的 interpreter 放在同一文件。)
名词解释
闭包(Closure):一个函数 + 它定义时所处的词法环境,打包在一起。函数无论被传到哪里调用,都能访问定义时看得到的那些变量。
引用环境 vs 调用环境:闭包捕获的是"定义处的环境"(引用),不是"调用处的环境"。这正是它和单纯"传值"不同的地方。
典型用途:工厂函数(如 makeCounter)、私有状态(变量不暴露在外层)、回调里保留上下文。
课后练习
练习:写一个 makeAdder(n),返回一个把参数加上 n 的函数,并验证 add5(10) === 15。
答案:
run(`
func makeAdder(n) {
return func(x) { return x + n; };
}
var add5 = makeAdder(5);
print(add5(10)); // 15
print(add5(3)); // 8
`)总结
闭包一点都不神秘,它就是 l4 讲的"Environment + parent 指针"的自然延伸:返回的函数带着它定义时的环境引用,这个环境函数调用结束后不会被回收,因为还有人(返回的函数)指着它。所谓"记住外部变量",本质是"外部变量所在的环境被闭包抓着"。理解了这点,私有变量、工厂函数、React 的自定义 Hook 状态保持,全都是同一个原理的不同包装。闭包不是语言给你加的魔法,是作用域链这一数据结构的必然产物——你早就拥有它了,只是现在叫出了名字。