A user_4a13383d 2026-09-07 05:20 6 次浏览 Javascript 闭包:函数为什么能记住已经离开的作用域 闭包(Closure)是“函数 + 它定义时可访问的词法环境”。常见情况是外层函数返回内层函数,内层函数仍引用外层变量;只要内层函数还可达,这些变量就会保持可访问,不会被立即垃圾回收。事件回调和定时器回调也可能形成闭包。 ## 目录 - [摘要](#摘要) - [一、先建立直觉:函数带走了什么](#一先建立直觉函数带走了什么) - [二、闭包是怎么形成的](#二闭包是怎么形成的) - [三、最小案例:做一个计数器](#三最小案例做一个计数器) - [四、闭包的三个实用场景](#四闭包的三个实用场景) - [五、边界问题:循环里的闭包](#五边界问题循环里的闭包) - [六、边界问题:闭包也会留住内存](#六边界问题闭包也会留住内存) - [七、使用闭包时的取舍](#七使用闭包时的取舍) - [八、总结](#八总结) ## 一、先建立直觉:函数带走了什么 先看一个普通的嵌套函数: ```javascript function outer() { const message = '你好,闭包'; function inner() { console.log(message); } inner(); } outer(); ``` `inner` 可以读取 `outer` 里的 `message`,这是词法作用域(Lexical Scope)的正常规则:函数在哪里定义,就能访问哪里的变量。 真正有意思的是下面这个版本。`outer` 执行结束后,`inner` 还被返回到了外面: ```javascript function outer() { const message = '你好,闭包'; return function inner() { return message; }; } const getMessage = outer(); console.log(getMessage()); // 你好,闭包 ``` 按直觉,`outer` 已经执行完了,`message` 似乎应该被清理。可 `getMessage` 仍然能读取它。这就是闭包的典型形成过程: > 一个函数,以及它定义时能够访问的词法环境,合在一起形成了闭包。 这里有两个容易混淆的点: - 闭包保存的不是变量当时的“照片”,而是对变量绑定的访问能力。 - 只有当外部函数里的变量被内层函数使用,并且内层函数逃到外部时,我们才会明显观察到闭包的效果。 ## 二、闭包是怎么形成的 可以把闭包的形成拆成三步: 1. 外层函数创建局部变量。 2. 内层函数引用这些变量。 3. 内层函数被返回、保存,或交给异步任务,于是它的生命周期超过了外层函数。 ```javascript function createReader() { let value = '初始值'; return function read() { return value; }; } const readValue = createReader(); console.log(readValue()); // 初始值 ``` 调用 `createReader()` 时,外层函数的执行上下文会结束,但 `readValue` 仍然引用 `value`。只要 `readValue` 还可达,这个变量就不能被当作“没人使用”来处理。 ### 闭包里的变量是“活的” 变量不是被复制到函数里的一份静态文本。内层函数和外层函数共享同一个变量绑定,因此修改会被后续调用看到: ```javascript function createScore() { let score = 0; return { add(point) { score += point; return score; }, current() { return score; }, }; } const score = createScore(); console.log(score.add(10)); // 10 console.log(score.add(5)); // 15 console.log(score.current()); // 15 ``` `add` 和 `current` 访问的是同一个 `score`,不是各自拿了一份副本。这个小柜子里只有一份账本,两位方法都在看它。 ## 三、最小案例:做一个计数器 先跑通一个最常见的闭包案例。为了先看懂主线,这里暂不处理并发、持久化和重置策略。 ```javascript function createCounter() { let count = 0; return { increase() { count += 1; return count; }, current() { return count; }, }; } const counter = createCounter(); console.log(counter.increase()); // 1 console.log(counter.current()); // 1 console.log(counter.increase()); // 2 ``` 外部代码不能直接写 `counter.count`,因为 `count` 只是 `createCounter` 内部的局部变量: ```javascript console.log(counter.count); // undefined ``` 这不是把数据变成了绝对安全的保险箱,而是把修改入口收窄了。外部只能通过 `increase` 和 `current` 操作状态,代码更容易追踪。 ### 每次调用都会得到独立状态 ```javascript const firstCounter = createCounter(); const secondCounter = createCounter(); firstCounter.increase(); firstCounter.increase(); secondCounter.increase(); console.log(firstCounter.current()); // 2 console.log(secondCounter.current()); // 1 ``` 每次调用 `createCounter` 都会创建一份新的词法环境,所以两个计数器互不串台。 ## 四、闭包的三个实用场景 ### 1. 封装私有状态 闭包可以隐藏状态,只公开必要的方法: ```javascript function createTodoStore() { const todos = []; return { add(title) { todos.push({ title, done: false }); }, list() { return todos.map((todo) => ({ ...todo })); }, }; } const store = createTodoStore(); store.add('读完闭包文章'); console.log(store.list()); ``` `list` 返回新对象,调用方不能通过修改返回结果,直接改掉仓库里的原始对象。这是“封装状态”和“控制数据出口”一起工作的例子。 ### 2. 创建函数工厂 如果多个函数只有一小段配置不同,可以用闭包批量生产它们: ```javascript function createPrefixer(prefix) { return (text) => prefix + text; } const warning = createPrefixer('[警告] '); const info = createPrefixer('[提示] '); console.log(warning('磁盘空间不足')); // [警告] 磁盘空间不足 console.log(info('任务已完成')); // [提示] 任务已完成 ``` `warning` 记住了 `[警告] `,`info` 记住了 `[提示] `。工厂函数负责生产,闭包负责把配置带到以后再用。 ### 3. 做简单缓存(记忆化) 闭包可以把缓存藏在函数内部,让重复输入直接命中结果: ```javascript function memoizeDouble() { const cache = new Map(); return (number) => { if (cache.has(number)) { return '命中缓存:' + cache.get(number); } const result = number * 2; cache.set(number, result); return '首次计算:' + result; }; } const double = memoizeDouble(); console.log(double(10)); // 首次计算:20 console.log(double(10)); // 命中缓存:20 ``` 这个案例故意保持简单。真实缓存通常还要考虑容量上限、过期时间和清理策略,否则闭包会很认真地把每个历史输入都记住。 ## 五、边界问题:循环里的闭包 问题出现了:`var` 在循环中只有一个函数作用域变量。三个回调最后都读取同一个 `i`,而循环结束时 `i` 已经是 `3`。 ```javascript const callbacksWithVar = []; for (var i = 0; i < 3; i += 1) { callbacksWithVar.push(() => i); } console.log(callbacksWithVar.map((callback) => callback())); // [3, 3, 3] ``` 这不是闭包失效了,恰恰是闭包太忠诚:它们都记住了同一个 `i`。只是大家共用一本账,最后看到的当然是同一个余额。 ### 修复一:优先使用 `let` `let` 在 `for` 循环中会为每次迭代提供独立的绑定: ```javascript const callbacksWithLet = []; for (let j = 0; j < 3; j += 1) { callbacksWithLet.push(() => j); } console.log(callbacksWithLet.map((callback) => callback())); // [0, 1, 2] ``` 这是现代 JavaScript 中最清楚的写法。变量只在循环内部使用时,直接用 `let`,不要让 `var` 出来制造悬疑。 ### 修复二:显式创建一层函数 如果需要兼容旧代码,也可以手动为每次迭代创建一个新的参数绑定: ```javascript function createCallback(value) { return () => value; } const callbacks = []; for (var index = 0; index < 3; index += 1) { callbacks.push(createCallback(index)); } console.log(callbacks.map((callback) => callback())); // [0, 1, 2] ``` 这个方案有效,但多了一层函数。新代码一般优先选择 `let`,把意图写得更直接。 ## 六、边界问题:闭包也会留住内存 闭包能延长变量的生命周期,这是优点,也是需要留意的成本。 ### 1. 先看一个容易忽略的引用链 ```javascript function createReporter() { const reportData = new Array(10000).fill('日志'); return function report() { return reportData.length; }; } const report = createReporter(); console.log(report()); // 10000 ``` 只要 `report` 还被保存,`reportData` 就可能继续存活。外层函数执行完,不代表闭包捕获的对象马上被垃圾回收。 ### 2. 事件监听器和定时器要能清理 常见问题是:组件销毁了,但事件监听器或定时器仍然引用着闭包。修复思路是返回清理函数,并在生命周期结束时调用它: ```javascript function bindResizeReporter(element) { let count = 0; function handleResize() { count += 1; console.log(element.tagName, count); } window.addEventListener('resize', handleResize); return function dispose() { window.removeEventListener('resize', handleResize); }; } const dispose = bindResizeReporter(document.body); // 页面或组件销毁时调用,解除事件监听器引用。 dispose(); ``` 这里的 `dispose` 仍然是闭包,但它的职责从“继续工作”变成了“解除引用”。具体项目也可能需要清理 `setInterval`、取消请求或释放订阅。 ### 3. 修复原则 - 只捕获真正需要的变量,避免把巨大的对象整体带进闭包。 - 长期保存的监听器、定时器、订阅和缓存,都要设计清理入口。 - 缓存要有容量或过期策略,不要让 `Map` 无限增长。 - 不要把闭包当成自动垃圾回收开关;是否回收仍取决于对象是否可达。 ## 七、使用闭包时的取舍 闭包很方便,但不是“所有状态管理问题的万能胶”。可以按下面的思路选择: | 需求 | 闭包是否合适 | 注意事项 | | --- | --- | --- | | 隐藏少量私有状态 | 合适 | 公开有限的读写方法 | | 根据配置批量生成函数 | 合适 | 明确配置的生命周期 | | 简单缓存计算结果 | 合适 | 增加容量、过期或清理策略 | | 管理大型共享状态 | 需要谨慎 | 考虑模块、类或专门的状态管理方案 | | 长期监听事件或定时器 | 可以使用 | 必须提供 `dispose` 或取消入口 | 判断一个闭包是否值得使用,可以问自己三个问题: 1. 这个变量是否真的需要跨越外层函数的执行时间? 2. 谁持有返回的函数,它会存活多久? 3. 状态是否有明确的修改入口和清理入口? ## 八、总结 - 闭包是“函数 + 它定义时可访问的词法环境”。 - 外层函数结束后,只要内层函数仍然可达,捕获的变量就可能继续存活。 - 闭包中的变量是共享的绑定,不是调用时复制的一份快照。 - 闭包适合封装私有状态、创建函数工厂和实现简单缓存。 - `var` 循环会让多个回调共享同一个变量;现代代码优先使用 `let`。 - 闭包可能延长对象生命周期,监听器、定时器和缓存必须设计清理策略。 点赞0 收藏0
评论
还没有评论,来分享你的想法吧。