深入理解 JavaScript 闭包
什么是闭包?
闭包(Closure)是 JavaScript 中最核心也最容易被误解的概念之一。它不是某个 API,也不是 ES6 新增的语法特性,而是由语言本身的词法作用域机制自然产生的现象。理解闭包,意味着你真正理解了 JavaScript 的作用域链和执行上下文。
闭包的定义
闭包是指一个函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
js
function outer() {
let count = 0
function inner() {
count++
console.log(count)
}
return inner
}
const fn = outer()
fn() // 1
fn() // 2
fn() // 3
这段代码中,inner 被返回到了外部,并在 outer 执行完毕后继续被调用。每次调用 fn(),count 都会自增------这就是闭包在起作用。
闭包的形成机制
词法作用域
JavaScript 采用词法作用域(静态作用域),函数的作用域在定义时就已确定,而非在调用时。
js
const x = 'global'
function foo() {
console.log(x) // 指向全局 x
}
function bar() {
const x = 'local'
foo() // 打印 "global"
}
函数作为一等公民
JavaScript 中的函数可以像普通值一样被赋值、传递参数、作为返回值。这使得函数可以"逃离"其定义时的作用域,带着它的作用域链一起。
执行上下文与作用域链
每个函数调用都会创建自己的执行上下文,其中包含词法环境的引用。闭包的特殊之处在于:即使外层函数的执行上下文已弹出调用栈,其变量对象仍然被内层函数引用着,因此不会被垃圾回收。
核心应用场景
1. 数据私有化
js
function createCounter() {
let _count = 0
return {
increment: () => ++_count,
decrement: () => --_count,
getCount: () => _count,
}
}
const counter = createCounter()
counter.increment() // 1
counter.increment() // 2
console.log(counter._count) // undefined
2. 函数工厂
js
function multiply(a) {
return function (b) {
return a * b
}
}
const double = multiply(2)
const triple = multiply(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
3. 模块模式(IIFE)
js
const myModule = (function () {
const privateKey = 'secret'
return {
publicMethod() {
console.log(privateKey)
},
}
})()
myModule.publicMethod()
4. 回调与事件处理
js
function setupButton(buttonId) {
let clickCount = 0
document.getElementById(buttonId).addEventListener('click', function () {
clickCount++
console.log('按钮被点击了', clickCount, '次')
})
}
闭包与内存
闭包会让变量无法被回收,如果滥用可能导致内存泄漏。
js
function createHeavyClosure() {
const heavyData = new Array(1000000).fill('data')
return function () {
console.log('hello') // 没用到 heavyData
}
}
// heavyData 永远不会被释放
解决:只保留需要的引用,或用完后手动解除。
循环中的经典问题
js
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i) // 打印 5 次 5
}, 1000)
}
修复方式:
- 使用 let
js
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000)
}
- IIFE 闭包
js
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => console.log(j), 1000)
})(i)
}
闭包与 React Hooks
Stale Closure
jsx
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count 始终是 0
}, 1000)
return () => clearInterval(timer)
}, [])
}
修复:使用函数式更新 setCount(prev => prev + 1)
useCallback 与闭包
jsx
const handleClick = useCallback(() => {
doSomething(dep)
}, [dep])
总结
闭包不是语言刻意设计的"特性",而是词法作用域与函数作为一等公民的自然结果。理解它的关键:
- 函数定义时确定作用域链,而非调用时
- 被返回的函数会"携带"其词法环境
- 数据私有化、函数工厂、模块模式的底层支撑
- 注意内存泄漏和循环中的闭包陷阱
- React Hooks 的本质也离不开闭包