函数记住的不是"调用时在哪", 而是"定义时在哪" — 作用域链在写代码那一刻就定型, 闭包是被函数抓住不放的环境
作用域是"户口": 函数在哪出生, 户口就落在哪一层, 查变量永远沿着出生地往上找 — 这就是词法作用域。而闭包是: 内层函数被返回到外地工作, 但它把出生地那个房间(变量环境)整个打包带走。房间里它用得着的变量count不会被回收, 函数每次回来都能接着上次继续。
一句话: 闭包让函数拥有私有、持久的记忆。防抖的 timer、缓存的上次结果、计数器的 count、模块的私有状态, 全是同一件事的不同马甲。代价也要记牢: 函数活着, 它抓住的整个环境就活着 — 内存泄漏的高发区。
let x = '全局'; function read() { return x; } // 定义处看到的 x 是全局这个 function wrap() { let x = '局部'; return read(); } wrap(); // → '全局', 不是 '局部'
const a = 1; function outer() { const b = 2; function inner() { return a + b; } // a 从全局, b 从 outer return inner; } outer()(); // → 3
function make() { const secret = 42; return () => secret; // 箭头函数抓住 secret } const read = make(); read(); // → 42, make 早已返回
const a = makeCounter(), b = makeCounter(); a(); a(); // a: count → 2 b(); // b: count → 1 互不影响
console.log(a); // → undefined (var 提升) var a = 1; console.log(b); // → ReferenceError (TDZ, 关键差异) let b = 2;
for (var i = 0; i < 3; i++) setTimeout(() => log(i)); // 3 3 3 for (let i = 0; i < 3; i++) setTimeout(() => log(i)); // 0 1 2
(function () { var private = 1; // 外界不可见 })(); // 关键: 括号把"声明"变"表达式"再调用
const wallet = (() => { let balance = 100; // 真私有 return { deposit: n => balance += n, get: () => balance }; })(); wallet.deposit(50); wallet.get(); // → 150
function process(big) { // big 是 100MB 数组 const n = big.length; // 只需要长度 return () => n; // 关键: 现代引擎可优化, 但别赌 } // 稳妥: 先提取标量, 别让闭包直接引用大对象
const onUpdate = () => render(cache.data); window.addEventListener('resize', onUpdate); // 组件销毁时: window.removeEventListener('resize', onUpdate); // 关键
let count = 0; btn.onclick = () => setTimeout(() => log(count)); count = 99; btn.click(); // → 99: 读的是同一变量; 但 React useState 的值是不可变快照 // hooks 场景: setCount(c => c + 1) 函数式更新避免 stale
{
// f(); // → ReferenceError: Cannot access 'f' before init
const f = () => 1; // 关键: 死区到这一行才结束
}输入联想每敲一键发一次请求, 服务器被打爆还闪屏。闭包保存 timer 与上下文, 静默期后才真正触发:
function debounce(fn, wait = 300) { let timer = null; // 闭包状态: 藏在返回函数里 return function (...args) { clearTimeout(timer); // 重置上一轮 timer = setTimeout(() => fn.apply(this, args), wait); }; } input.addEventListener('input', debounce(e => search(e.target.value)));
请求量从"每键一次"降到"停手一次", QPS 降 90%+; apply(this, args) 保住调用方上下文。
价格计算函数每次都要拉汇率、跑 200ms, 列表页一次调 500 次。闭包字典做结果缓存:
function memoize(fn) { const cache = new Map(); // 闭包持有, 外部摸不到 return (...args) => { const key = JSON.stringify(args); if (!cache.has(key)) cache.set(key, fn(...args)); return cache.get(key); // 关键: 同参直接命中 }; } const quote = memoize(calcPrice);
同参命中率 80%+, 页面计算耗时从 10s 降到 2s; 注意缓存键要含全部影响结果的入参。
"提交"按钮连点 3 次生成 3 笔订单; SDK init 被多处调用重复连接。闭包守卫只放行第一次:
function once(fn) { let called = false, result; return (...args) => { if (called) return result; // 关键: 之后全返回首跑结果 called = true; return (result = fn(...args)); }; } const submit = once(async (form) => createOrder(form));
批量导入进度用 setCount(count + 1), 连续触发时读到的都是旧快照, 100 次只 +37:
// 错: count 是本次渲染的快照, 批量更新时已过期 onChange={() => setCount(count + 1)} // 对: 函数式更新, 拿到队列里最新的值 onChange={() => setCount(c => c + 1)} // 跨渲染读最新值(如定时器里), 用 ref: const latest = useRef(count); latest.current = count; useEffect(() => { const id = setInterval(() => report(latest.current), 1000); return () => clearInterval(id); }, []);
1000 行表格每行删除按钮要带自己的 id, 传统做法 dataset 存值再解析, 闭包直接捕获行对象更干净:
rows.forEach(row => {
const btn = el('button', '删除');
btn.onclick = () => removeRow(row.id); // 关键: 每轮闭包捕获自己的 row
tr.append(btn);
});
// 注意: 重建列表前先解绑, 否则旧 row 全被拽在闭包里不释放
埋点接口 20 个参数, 业务侧只想传事件名与属性。闭包固定公共参数, 生成专用函数:
const track = (common) => (event, props = {}) => beacon({ ...common, event, ...props }); const trackApp = track({ app: 'mall', ver: '2.3.0' }); trackApp('add_cart', { sku: 'A1' }); // 公共字段自动带上
模块级缓存闭包让测试互相污染: 用例 A 写入的缓存被用例 B 命中, 单跑绿合跑红。工厂化让每例拿到全新实例:
// 错: 模块顶层创建, 所有测试共享同一个 cache 闭包 export const store = createStore(); // 对: 导出工厂, 测试 setup 里各造各的 export const createStore = () => { let cache = new Map(); return { get: k => cache.get(k), set: (k, v) => cache.set(k, v) }; }; // 每个用例: const store = createStore(); 隔离干净
切换 20 个报表后页面上 GB。heap snapshot 显示大量 Retained by "refreshChart" 闭包 — 监听器没解绑, 大数据被拽住:
// 泄漏形态: 全局事件持有含 50MB 数据的闭包 window.addEventListener('resize', () => chart.refresh(bigData)); // 修复: 命名函数 + 生命周期对称解绑 const onResize = () => chart.refresh(bigData); window.addEventListener('resize', onResize); onUnmount(() => window.removeEventListener('resize', onResize));
切换 50 次内存曲线平稳; 排查路径: Memory 面板拍快照 → 按 Retainers 找闭包 → 对应解绑。
类字段 #count 与闭包都能私有, 选型看需求: 闭包适合少量实例/函数式, #field 适合类体系且调试器可见:
// 闭包版: 状态在构造器作用域里, 调试面板看 Closure class RateLimiter1 { constructor(max) { let used = 0; this.tryAcquire = () => used < max && (used++, true); } } // #field 版: 真·对象私有, 子类不可见但 DevTools 直接可查 class RateLimiter2 { #used = 0; #max; constructor(max) { this.#max = max; } tryAcquire() { return this.#used < this.#max && (this.#used++, true); } }
Node 服务里模块级 let currentUser 被所有请求共享, 高并发下串号。请求级状态必须每请求新建:
// 错: 模块级闭包状态, 全进程共享 let currentUser = null; app.use((req, res, next) => { currentUser = req.user; next(); }); // 对: 状态挂到请求对象/AsyncLocalStorage, 每请求独立 const { AsyncLocalStorage } = require('async_hooks'); const als = new AsyncLocalStorage(); app.use((req, res, next) => als.run({ user: req.user }, next)); // 任意深度: als.getStore().user —— 跟踪异步链且请求间隔离
let 每轮新绑定。 // 错: for (var i = 0; i < 3; i++) setTimeout(() => log(i)); // 3 3 3 // 对: for (let i = 0; i < 3; i++) setTimeout(() => log(i)); // 0 1 2
// 错: () => rows.length + total(rows) // 整个 100MB rows 被拽住 // 对: const n = rows.length; () => n; // 只留数字
removeEventListener 或 AbortSignal。 // 错: 匿名函数无法解绑 window.addEventListener('resize', () => render()); // 对: const h = () => render(); addEventListener('resize', h); onUnmount(() => removeEventListener('resize', h));
useRef 镜像最新值。 // 错: setCount(count + 1) // count 是过期快照 // 对: setCount(c => c + 1) // 队列里最新值
// 错: helper(); const helper = () => {}; // → ReferenceError // 对: const helper = () => {}; helper();
(function(){})() 两对括号缺一不可。 // 错: (function () { init(); }); // 没执行! // 对: (function () { init(); })(); // 立即执行
'use strict' 或统一 ESM(自带严格模式)。 // 错: function f() { total = 1; } // window.total = 1 泄漏 // 对: function f() { let total = 1; }
const inc1 = make(), inc2 = make(); // 两个独立环境 const [a, b] = [make(), make()]; // 同上, 别共享
Map。 // 错: eval('cfg.' + key); // 慢+不安全+作用域混乱 // 对: cfg[key] // 或 new Map([[key, val]])
// 错: 两个 async 回调同时 read-modify-write seq // 对: 串行化 let chain = Promise.resolve(); const run = task => (chain = chain.then(task));
// 错: setTimeout(() => log(cfg), 0); cfg = '改了'; // 读到'改了' // 对: const snap = cfg; setTimeout(() => log(snap), 0);
#field 真私有。 // 错: this._secret = 1; // 外部 obj._secret = 2 直接改掉 // 对: #secret = 1; // 外部访问直接语法错误
dataset 读参数。 // 错: 1000 行 1000 个闭包 // 对: 委托 table.onclick = e => { const id = e.target.dataset.id; if (id) removeRow(id); };
function(...args) + fn.apply(this, args)。 // 错: return (...a) => fn(...a); // this 丢失 // 对: return function (...a) { fn.apply(this, a); };
// 错: (opts) => { opts.ready = true; } // 调用方的对象被改 // 对: (opts) => { const o = { ...opts, ready: true };
AsyncLocalStorage/req 对象)。 // 错: 模块顶层 let user 全进程共享 // 对: als.run({ user }, handler) 请求级隔离
let x; for (x = 0; x < 3; x++) setTimeout(() => log(x)); // 3 3 3 for (let x = 0; x < 3; x++) setTimeout(() => log(x)); // 0 1 2
// 断点在 inner: Scope → Closure 只有被引用的 // outer 的 localVar 没被 inner 用 → 不在, 不是丢了
// 错: cache.set 无限累积 // 对: if (cache.size > 500) cache.delete(cache.keys().next().value);
clearInterval。 // 错: setInterval(() => setState(data), 1000) 没清理 // 对: useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }, []);