闭包是 JavaScript 中最基础、最强大的特性之一。当函数在另一个函数内部定义,并且即使在外部函数返回后仍然保留对外部函数变量的访问权限时,就会创建闭包。深入理解闭包将改变你构建 JavaScript 代码的方式——从模块模式到 React hooks。
什么是闭包?
每次在 JavaScript 中创建函数时,都会形成一个闭包。函数"关闭"了其周围的词法环境——函数定义时作用域内的变量。
// What is a closure?
// A closure is a function that has access to variables
// from its outer (enclosing) lexical scope, even after
// that scope has returned.
function makeCounter() {
let count = 0; // private variable in the outer scope
return function() {
count++; // inner function accesses outer variable
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is NOT accessible from outside:
// console.log(count); // ReferenceError!
// Each call to makeCounter() creates a NEW closure
const counter2 = makeCounter();
console.log(counter2()); // 1 (independent state)
console.log(counter()); // 4 (original counter continues)作用域链和变量捕获
闭包通过引用捕获变量,而非按值捕获。这既是力量的来源,也是常见错误的根源。
// Closure captures the ENTIRE scope chain, not just one level
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
// Has access to ALL enclosing scopes
console.log(globalVar); // 'global'
console.log(outerVar); // 'outer'
console.log(middleVar); // 'middle'
}
inner();
}
middle();
}
outer();
// Variables are captured by REFERENCE, not by value
function makeAdder(x) {
// x is captured by reference
return function(y) {
return x + y; // x is still accessible
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3)); // 8
console.log(add10(3)); // 13经典循环错误(及修复方法)
最臭名昭著的闭包错误:在循环中使用 var 配合异步回调。理解这个问题能揭示闭包如何捕获变量。
// CLASSIC BUG: var in loops (before let/const existed)
// Wrong: all callbacks share the same i variable
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3 (not 0, 1, 2!)
}, 100);
}
// Reason: var is function-scoped, one shared i that becomes 3
// Fix 1: use let (creates a new binding per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2 ✓
}, 100);
}
// Fix 2: IIFE (Immediately Invoked Function Expression) — old school
for (var i = 0; i < 3; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI); // prints 0, 1, 2 ✓
}, 100);
})(i);
}
// Fix 3: factory function
function makeTimeout(index) {
return function() {
console.log(index); // captured by value in this scope
};
}
for (var i = 0; i < 3; i++) {
setTimeout(makeTimeout(i), 100); // 0, 1, 2 ✓
}实用的闭包模式
闭包支持几种强大的编程模式:模块模式、记忆化、偏应用等。
// CLOSURE PATTERNS
// 1. Module Pattern — private state with public API
const bankAccount = (function() {
let balance = 0; // private
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
})();
bankAccount.deposit(100);
bankAccount.withdraw(30);
console.log(bankAccount.getBalance()); // 70
// console.log(balance); // ReferenceError
// 2. Memoization — cache expensive computations
function memoize(fn) {
const cache = new Map(); // cache lives in the closure
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('cache hit');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const slowFibonacci = (n) => n <= 1 ? n : slowFibonacci(n - 1) + slowFibonacci(n - 2);
const fastFibonacci = memoize(slowFibonacci);
// 3. Partial application
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const multiply = (a, b) => a * b;
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 4. Once — run a function only one time
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initialized!');
return { status: 'ready' };
});
initialize(); // Initialized!
initialize(); // (nothing logged)
initialize(); // (nothing logged)闭包的内存管理
闭包保持对其封闭作用域的引用。这是闭包工作的必要条件,但如果不仔细管理,可能会导致内存泄漏。
// MEMORY CONSIDERATIONS
// Closures keep references alive — potential memory leaks
// BAD: Large object kept alive by closure
function processData(hugeArray) {
// processedResult only needs a small piece of hugeArray
const processedResult = hugeArray[0].value;
// But this closure keeps hugeArray alive in memory
return function getResult() {
return processedResult; // only uses processedResult
// However, the closure scope contains hugeArray!
};
}
// BETTER: Narrow the scope so hugeArray can be GC'd
function processDataFixed(hugeArray) {
const processedResult = hugeArray[0].value;
hugeArray = null; // allow GC (if no other references)
return function getResult() {
return processedResult;
};
}
// REAL-WORLD: Event listener leaks
class Component {
constructor() {
this.data = new Array(1000000).fill('x'); // large data
this.handler = (event) => {
console.log(this.data.length); // closure over 'this'
};
document.addEventListener('click', this.handler);
}
// MUST clean up to prevent memory leak
destroy() {
document.removeEventListener('click', this.handler);
}
}React 中的闭包:陈旧闭包问题
React hooks 严重依赖闭包,这可能导致"陈旧闭包"问题,即事件处理器或副作用捕获了过时的状态。
// Closures in React — the stale closure problem
import { useState, useEffect, useCallback, useRef } from 'react';
// PROBLEM: stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// count is captured at the time the effect runs (= 0)
console.log(count); // always logs 0! (stale closure)
}, 1000);
return () => clearInterval(interval);
}, []); // empty deps = runs once = captures count=0
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// FIX 1: Add count to dependency array
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // fresh value (but re-creates interval)
}, 1000);
return () => clearInterval(interval);
}, [count]);
// FIX 2: Use a ref (mutable, doesn't trigger re-render)
function CounterWithRef() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // always up to date
useEffect(() => {
const interval = setInterval(() => {
console.log(countRef.current); // always fresh!
}, 1000);
return () => clearInterval(interval);
}, []); // no deps needed
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}闭包 vs 类:何时使用哪个
| Aspect | Closure | Class |
|---|---|---|
| Private state | Local variables in factory function | Private class fields (#field) |
| Memory (single instance) | More efficient (no prototype) | Less efficient (prototype overhead) |
| Memory (many instances) | Less efficient (each has own methods) | More efficient (shared prototype methods) |
| Inheritance | Via composition / Object.assign | Via extends keyword |
| this binding | No this issues (functional style) | Requires careful this management |
| Readability | Functional, compact | OOP, familiar to Java/C# devs |
| Performance | Slightly faster for closures | Slightly faster method calls |
常见问题
闭包和 IIFE(立即调用函数表达式)一样吗?
不一样,但有关联。IIFE 是定义后立即调用的函数。它创建一个新作用域,从它返回的任何函数都会对该作用域形成闭包。在 ES 模块和块作用域 let/const 之前,IIFE 是创建私有作用域的主要方式。今天,ES 模块提供了更好的封装。
箭头函数会创建闭包吗?
是的。就闭包而言,箭头函数没有什么特别之处——它们像普通函数一样从周围的词法作用域捕获变量。关键区别在于箭头函数没有自己的 this 绑定,所以它们也从周围作用域捕获 this。
闭包和类有什么区别?
两者都可以维护私有状态,但方法不同。闭包使用函数作用域封装状态——私有变量只是工厂函数中的局部变量。类使用类字段(现代 JS 中私有字段用 # 前缀)。创建一个实例时,闭包更节省内存;对于多个实例,类(使用原型)更节省内存,因为方法在原型上共享。
闭包与垃圾回收有什么关系?
只要闭包本身可以被访问,它就会保持其封闭作用域。如果你创建了一个引用大型对象的闭包,只要闭包存在,该对象就不会被垃圾回收。这是闭包相关内存泄漏的主要来源。当事件监听器和定时器不再需要时,始终移除它们。
为什么 React hooks 依赖闭包?
useState 和 useEffect 等 React hooks 返回的函数会关闭组件在渲染时的状态和 props。每次渲染都会创建一个包含当前状态快照的新闭包。这就是为什么 useEffect 依赖项很重要——在依赖数组中包含陈旧值确保 React 在这些值改变时使用新鲜闭包重新运行副作用。