重构一个用户权限系统时,遇到一个新人写的代码:
js
function createUserSession(userData) {
const user = userData
const loginTime = Date.now()
let token = null
return {
setToken(t) { token = t },
getToken() { return token },
getUser() { return user },
getAge() {
return new Date().getFullYear() - user.birthYear
}
}
}
const session = createUserSession({
name: 'Alice',
birthYear: 1995,
role: 'admin'
})
session.setToken('abc123')
console.log(session.getToken()) // 'abc123'
他问我:"为什么 token
变量在函数执行完后还能被访问?它不应该被销毁吗?"
这就是 JavaScript 中最强大也最让人困惑的概念------闭包(Closure)。
一、问题场景:如何安全地管理用户会话
我们有个后台管理系统,需要:
- 存储用户敏感信息(如 token)
- 提供操作接口(获取/更新 token)
- 禁止外部直接访问原始数据
- 支持多用户独立会话
如果不用闭包,可能会这样写:
js
// ❌ 全局变量污染 + 数据不安全
let currentUser = null
let currentToken = null
function login(user, token) {
currentUser = user
currentToken = token
}
但这样任何人都可以直接修改 currentToken
,太危险了。
二、解决方案:用闭包创建私有作用域
我们用闭包重构:
js
function createAuthManager(initialUser) {
// 🔍 私有变量:外部无法直接访问
let user = initialUser
let token = null
const createdAt = Date.now()
// 🔐 特权函数:可以访问私有变量的公共接口
return {
// 设置 token(带验证)
setToken(t) {
if (!t || typeof t !== 'string') {
throw new Error('Invalid token')
}
token = t
console.log('Token updated')
},
// 获取 token
getToken() {
return token ? token.slice(0, 5) + '...' : null // 🔐 只返回部分信息
},
// 获取用户信息
getUser() {
// 返回副本,防止外部修改原始数据
return { ...user }
},
// 更新用户信息
updateUser(updates) {
user = { ...user, ...updates }
},
// 检查是否登录
isLoggedIn() {
return !!token && (Date.now() - createdAt) < 8 * 60 * 60 * 1000 // 8小时有效期
},
// 登出
logout() {
token = null
}
}
}
使用方式:
js
const auth = createAuthManager({
name: 'Alice',
role: 'admin',
birthYear: 1995
})
auth.setToken('jwt-token-12345')
console.log(auth.getToken()) // 'jwt-t...'
console.log(auth.getUser().name) // 'Alice'
auth.logout()
console.log(auth.isLoggedIn()) // false
三、原理剖析:从表面到内存管理的三层机制
1. 表面用法:什么是闭包?
当一个函数记住并访问它的词法作用域,即使这个函数在它的词法作用域外执行时,就产生了闭包。
我们来画一张 闭包内存引用图:
graph TB
A["[全局执行上下文]"] --> B["createAuthManager() 执行"]
B --> C["创建局部变量:user, token, createdAt"]
C --> D["返回对象(包含函数)"]
D --> E["createAuthManager 执行结束"]
E --> F["本该销毁局部变量"]
F --> G["但!返回的函数内部引用了这些变量"]
G --> H["JavaScript 引擎决定:保留这些变量在内存中"]
H --> I["形成闭包:函数 + 词法环境的组合"]
style A fill:#9f9,stroke:#333,stroke-width:2px
style F fill:#ffd699,stroke:#333,stroke-width:2px
style G fill:#f99,stroke:#333,stroke-width:2px
style I fill:#d4edda,stroke:#333,stroke-width:2px
2. 底层机制:V8 引擎如何处理闭包?
当函数被定义时,JavaScript 会创建一个隐藏属性 [[Environment]]
,指向定义时的词法环境。
js
function outer() {
const secret = 'I am private'
function inner() {
console.log(secret) // inner.[[Environment]] → 指向 outer 的作用域
}
return inner
}
即使 outer
执行完毕,inner
仍然通过 [[Environment]]
引用着 secret
变量,因此垃圾回收器不会清理它。
3. 设计哲学:为什么需要闭包?
闭包实现了 数据封装 和 状态持久化,解决了三个核心问题:
问题 | 闭包解决方案 |
---|---|
全局变量污染 | 把数据放在函数作用域内 |
数据不安全 | 外部无法直接访问私有变量 |
状态丢失 | 函数执行完后状态仍保留 |
💡 类比:
闭包就像一个"带锁的保险箱"------
- 箱子(函数作用域)里放着贵重物品(变量)
- 只有特定钥匙(返回的函数)才能打开箱子
- 即使你把钥匙交给别人,他也只能按规则取物,不能拆箱子
四、闭包的典型使用场景
1. 模块模式(Module Pattern)
js
const Counter = (function() {
let count = 0 // 私有状态
return {
increment() { return ++count },
decrement() { return --count },
value() { return count },
reset() { count = 0 }
}
})()
2. 函数工厂(Function Factory)
js
function createMultiplier(factor) {
return function(number) {
return number * factor
}
}
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
3. 事件处理与回调
js
function setupButton(id, label) {
const element = document.getElementById(id)
element.addEventListener('click', function() {
// 🔍 这里形成了闭包,可以访问 label 变量
console.log(`Button ${label} clicked`)
})
}
4. 防抖与节流
js
function debounce(func, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => func.apply(this, args), delay)
}
}
const saveDraft = debounce(() => {
console.log('Saving draft...')
}, 1000)
五、特权函数的使用场景
"特权函数"是指那些既能被外部调用,又能访问私有变量的函数。它们是闭包最强大的应用。
场景1:带验证的数据访问器
js
function createBankAccount(initialBalance) {
let balance = initialBalance
return {
// 特权函数:带验证的取款
withdraw(amount) {
if (amount > balance) {
throw new Error('余额不足')
}
balance -= amount
return balance
},
// 特权函数:带日志的存款
deposit(amount) {
console.log(`存入 ${amount}`)
balance += amount
return balance
},
// 只读访问
getBalance() {
return balance
}
}
}
场景2:配置管理器
js
function createConfigManager(defaults) {
let config = { ...defaults }
return {
// 合并新配置
update(newConfig) {
config = { ...config, ...newConfig }
},
// 获取配置(防止外部修改)
get(key) {
return key ? config[key] : { ...config }
},
// 重置
reset() {
config = { ...defaults }
}
}
}
场景3:单例模式
js
const Logger = (function() {
let instance = null
let logs = []
function log(message) {
logs.push({
timestamp: Date.now(),
message
})
console.log(message)
}
return {
getInstance() {
if (!instance) {
instance = { log, getLogs: () => [...logs] }
}
return instance
}
}
})()
六、实战避坑指南
❌ 错误用法:在循环中创建闭包不注意绑定
js
// 常见错误
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i) // 输出 3, 3, 3
}, 100)
}
✅ 正确做法:使用 let 或 IIFE
js
// 方案1:用 let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i) // 0, 1, 2
}, 100)
}
// 方案2:用 IIFE
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => {
console.log(index)
}, 100)
})(i)
}
❌ 错误用法:闭包导致内存泄漏
js
function handleHugeData() {
const hugeData = new Array(1000000).fill('data')
// 即使不需要 hugeData 了,只要这个事件监听器存在,它就不会被回收
window.addEventListener('resize', () => {
console.log('Resize')
})
}
✅ 正确做法:及时解绑或置空
js
function handleHugeData() {
const hugeData = new Array(1000000).fill('data')
let handler
handler = () => {
console.log('Resize')
// 使用完后解绑
window.removeEventListener('resize', handler)
// 帮助垃圾回收
handler = null
hugeData = null
}
window.addEventListener('resize', handler)
}
七、举一反三:三个变体场景实现思路
-
需要多个实例共享私有数据
在外层再包一层闭包,让多个实例共享同一个私有作用域(如连接池管理)。
-
实现私有类字段(ES6 Class)
结合 WeakMap 实现真正的私有属性,避免闭包长期持有引用。
-
缓存计算结果(记忆化)
用闭包保存计算结果,避免重复运算。
js
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
小结
闭包不是"意外的功能",而是 JavaScript 中实现 封装、状态管理和模块化的核心机制。
记住这个口诀:
函数内变量,函数外还能用,
作用域链锁,内存中永留存。
当你需要:
- 创建私有变量
- 保持状态
- 生成定制函数
- 实现模块化
闭包就是你的第一选择。
而特权函数,就是你暴露给世界的"安全接口"------它既能保护内部数据,又能提供灵活的操作能力。