闭包与作用域
闭包是 JavaScript 最核心的特性之一,也是函数式编程、模块化、React Hooks 的底层基础。
作用域链
JavaScript 使用词法作用域(静态作用域)——函数的作用域在定义时确定,而非调用时。
js
const x = 'global'
function outer() {
const x = 'outer'
function inner() {
const x = 'inner'
console.log(x) // 'inner'(就近原则)
}
function inner2() {
console.log(x) // 'outer'(向上查找)
}
inner()
inner2()
}
outer()作用域链查找过程:
inner2 查找 x:
inner2 自身作用域 → 没有
outer 作用域 → 找到 'outer' ✓
(不会继续向上查找)闭包的定义
闭包 = 函数 + 其词法环境(Lexical Environment)的引用
当一个函数能访问其定义时所在作用域的变量,即使该作用域已经执行完毕,这个函数就形成了闭包。
js
function makeCounter() {
let count = 0 // 这个变量被闭包"捕获"
return {
increment() { count++ },
decrement() { count-- },
value() { return count }
}
}
const counter = makeCounter()
// makeCounter 已执行完毕,但 count 仍然存活
counter.increment()
counter.increment()
console.log(counter.value()) // 2闭包的内存模型
makeCounter 执行完毕后:
堆内存中:
┌─────────────────────────────┐
│ Closure Environment │
│ { count: 2 } │ ← 被三个方法引用,不会被 GC
└──────┬──────────────────────┘
│ 引用
┌──────▼──────────────────────┐
│ { increment, decrement, │
│ value } │ ← counter 变量持有
└─────────────────────────────┘经典陷阱:循环中的闭包
js
// ❌ 经典错误
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:3 3 3(所有回调共享同一个 i)
// ✅ 方案一:let(块级作用域,每次循环创建新绑定)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:0 1 2
// ✅ 方案二:IIFE 创建独立作用域
for (var i = 0; i < 3; i++) {
;(function(j) {
setTimeout(() => console.log(j), 0)
})(i)
}
// 输出:0 1 2
// ✅ 方案三:bind 绑定参数
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 0)
}实用模式
模块模式(IIFE)
js
const userModule = (() => {
// 私有变量,外部无法访问
let _users = []
let _nextId = 1
// 私有方法
function _validate(user) {
return user.name && user.email
}
// 公开 API
return {
add(user) {
if (!_validate(user)) throw new Error('Invalid user')
_users.push({ ...user, id: _nextId++ })
},
getAll() {
return [..._users] // 返回副本,防止外部修改
},
count() {
return _users.length
}
}
})()
userModule.add({ name: 'Alice', email: 'alice@example.com' })
console.log(userModule.count()) // 1
// _users 无法从外部访问函数工厂
js
// 创建带有预设参数的函数
function multiply(factor) {
return (number) => number * factor
}
const double = multiply(2)
const triple = multiply(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
// 实际应用:事件处理器工厂
function createHandler(type) {
return function(event) {
console.log(`${type} event:`, event.target)
// 每个 handler 都记住了自己的 type
}
}
button.addEventListener('click', createHandler('click'))
input.addEventListener('input', createHandler('input'))记忆化(Memoization)
js
function memoize(fn) {
const cache = new Map() // 闭包捕获 cache
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
}
}
const expensiveCalc = memoize((n) => {
console.log(`计算 ${n}...`)
return n * n
})
expensiveCalc(4) // 计算 4... → 16
expensiveCalc(4) // 直接返回 16(缓存命中)
expensiveCalc(5) // 计算 5... → 25偏函数应用(Partial Application)
js
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs)
}
}
function add(a, b, c) {
return a + b + c
}
const add5 = partial(add, 2, 3) // 预设 a=2, b=3
console.log(add5(10)) // 15
// 实际应用:API 请求封装
const apiRequest = partial(fetch, 'https://api.example.com')
apiRequest('/users') // fetch('https://api.example.com', '/users')React Hooks 中的闭包
React Hooks 大量使用闭包,理解闭包是避免 Hooks 陷阱的关键:
jsx
// ❌ 闭包陷阱:stale closure
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
// 这里的 count 是闭包捕获的初始值 0,永远不会更新
setCount(count + 1) // 始终是 0 + 1 = 1
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖数组,effect 只运行一次
return <div>{count}</div>
}
// ✅ 正确:使用函数式更新,不依赖闭包中的 count
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // 使用最新的 prev
}, 1000)
return () => clearInterval(timer)
}, [])
return <div>{count}</div>
}闭包与内存泄漏
js
// ⚠️ 潜在内存泄漏:大对象被闭包意外持有
function createLeak() {
const bigData = new Array(1000000).fill('data') // 大数组
return function() {
// 即使只用到 bigData.length,整个 bigData 都不会被 GC
console.log(bigData.length)
}
}
// ✅ 只保留需要的数据
function createNoLeak() {
const bigData = new Array(1000000).fill('data')
const length = bigData.length // 只保留需要的值
// bigData 可以被 GC 了
return function() {
console.log(length)
}
}总结
- 闭包 = 函数 + 词法环境,让函数"记住"定义时的作用域
var在循环中共享变量,用let或 IIFE 解决- 闭包是模块化、函数工厂、记忆化的基础
- React Hooks 依赖闭包,注意 stale closure 问题
- 避免在闭包中意外持有大对象导致内存泄漏