💡 本篇目标:从编译原理层面彻底搞懂闭包的本质;结合实际开发中的 5 类典型应用场景,剖析闭包的价值与风险;并指出常见误用与内存泄漏问题。
🎯 一、闭包是什么?为什么这么多人搞不懂?
📖 定义:
闭包是函数能够“记住并访问”它定义时所在作用域的变量,即使函数是在其作用域外被调用。
🚨 误区澄清:
- ❌ 闭包不是“函数中嵌套函数”
- ✅ 闭包是函数 + 其定义时作用域的组合
🧠 二、本质:函数内部持有外部变量的引用
function outer() {
const message = 'Hello'
return function inner() {
console.log(message)
}
}
const fn = outer()
fn() // 输出 'Hello'
✅ 内部结构抽象(简化):
fn → {
code: function inner() {...},
[[Environment]]: {
message: 'Hello'
}
}
- inner 函数执行时,依赖
[[Environment]]访问其外部变量 - 这就是闭包机制
🔍 三、闭包的 5 大实战应用
1️⃣ 封装私有变量
function createCounter() {
let count = 0
return {
inc: () => ++count,
get: () => count
}
}
const c = createCounter()
c.inc() // 1
c.inc() // 2
🧠 应用:模块化、数据保护、限制访问权限
2️⃣ 模拟 once 执行器(只执行一次)
function once(fn) {
let called = false
return function (...args) {
if (!called) {
called = true
return fn.apply(this, args)
}
}
}
const fire = once(() => console.log('🔥 Fire!'))
fire() // 打印
fire() // 忽略
🧠 应用:登录按钮防多次点击、API 初始化控制
3️⃣ 延迟执行 & 缓存数据
function memoizedAdd() {
let cache = {}
return (n) => {
if (n in cache) return cache[n]
const result = n + 10
cache[n] = result
return result
}
}
const add = memoizedAdd()
add(5) // 15
add(5) // 缓存返回
🧠 应用:缓存中间结果,提高性能
4️⃣ 定时器或异步中的“变量保留”
function delayLog() {
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
}
delayLog() // 输出 3 3 3 ❌
✅ 修正方法一:立即执行函数 IIFE
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(() => console.log(i), 1000)
})(i)
}
✅ 方法二:使用 let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000)
}
5️⃣ 事件监听器封装
function bindEvent(elem, type, countLimit) {
let count = 0
function handler() {
if (++count <= countLimit) {
console.log(`第${count}次点击`)
} else {
elem.removeEventListener(type, handler)
}
}
elem.addEventListener(type, handler)
}
🧠 应用:限制触发次数、防止频繁绑定解绑逻辑
🧨 四、闭包的常见陷阱
❌ 1. 循环中创建闭包绑定 var
var fns = []
for (var i = 0; i < 3; i++) {
fns.push(() => console.log(i))
}
fns[0]() // 3 ❌
原因:所有箭头函数共享同一个
i引用,循环结束后 i = 3
✅ 解法:let / IIFE
❌ 2. 闭包导致内存泄漏(被意外引用)
function bindTooltip(elem) {
const tooltip = document.createElement('div')
tooltip.innerText = '信息提示'
elem.addEventListener('mouseenter', function () {
tooltip.style.display = 'block'
})
// ❗未移除 listener,即便 DOM 删除,tooltip 被引用着
}
🧽 最佳实践:
- 移除事件监听器
- 避免无谓引用外部变量(如 DOM)
❌ 3. 滥用闭包导致性能下降
function createButtonHandlers() {
const handlers = []
for (let i = 0; i < 1000; i++) {
handlers.push(() => console.log(i))
}
return handlers
}
- 过度使用闭包,导致 1000 个函数实例驻留内存
- 可以考虑统一 handler + 传参方式优化
📦 五、工程实践建议
| 使用闭包的目的 | 推荐做法 |
|---|---|
| 数据封装 | 使用返回函数的形式 |
| 事件绑定 | 保证解除绑定或使用 WeakMap |
| 模块缓存 | 保证内存控制,设置失效机制 |
| 大量闭包创建 | 尽量复用 handler,减少不必要引用 |
🧭 总结回顾
- 闭包是函数 + 环境变量记录的组合
- 是 JS 模块封装、延迟执行、权限控制的核心机制
- 常见陷阱包括:循环 var、未释放引用、内存泄漏
- 项目中使用闭包要明确边界与释放策略
📘 下一篇《第4篇:let/const 暗藏的块级作用域行为》,我们将深入探讨为什么 let 是闭包场景的“神兵利器”,并解析 TDZ、块作用域、循环变量绑定等问题。