JavaScript 闭包详解:工作原理与重要性
闭包是 JavaScript 面试中最常被问到的主题之一,也是最容易被误解的概念之一。一旦你真正理解了它们,它们就会成为你每天使用的强大工具。
什么是闭包?
闭包是一个函数,它记住了定义时所在作用域的变量,即使该作用域已经执行完毕。
function makeCounter() {
let count = 0; // 这个变量被“封闭”了
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 外部无法访问 count!
console.log(count); // ReferenceError: count is not defined
内部函数“封闭”了 count 变量。即使 makeCounter() 返回后,内部函数仍然可以访问 count。
闭包如何工作:词法作用域
JavaScript 使用词法作用域:函数的作用域由它在代码中的编写位置决定,而不是调用位置。
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
// inner() 沿着作用域链查找:inner → outer → global
console.log(name); // 'Outer'(不是 'Global')
}
inner();
}
outer();
inner() 的作用域链是:
inner自身的作用域outer的作用域 ← 在这里找到name- 全局作用域
常见用例
1. 数据隐私 / 模块模式
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有——无法直接访问
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
// 无法直接访问 balance!
console.log(account.balance); // undefined
2. 工厂函数
function createMultiplier(factor) {
// factor 被封闭
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenX = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
3. 记忆化(缓存)
function memoize(fn) {
const cache = new Map(); // 被返回的函数封闭
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 expensiveCalc = memoize((n) => {
// 模拟耗时的计算
let result = 0;
for (let i = 0; i < n * 1000000; i++) result += i;
return result;
});
expensiveCalc(10); // 计算(慢)
expensiveCalc(10); // 缓存命中!(瞬间)
4. 偏函数应用
function multiply(a, b) {
return a * b;
}
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const multiplyByFive = partial(multiply, 5);
console.log(multiplyByFive(3)); // 15
console.log(multiplyByFive(10)); // 50
5. 带状态的事件处理器
function createButton(label) {
let clickCount = 0;
const button = document.createElement('button');
button.textContent = label;
button.addEventListener('click', function() {
// 这个处理器封闭了 clickCount
clickCount++;
console.log(`"${label}" clicked ${clickCount} times`);
});
return button;
}
const btn1 = createButton('Save'); // 有自己的 clickCount
const btn2 = createButton('Delete'); // 有自己的 clickCount
经典的循环 Bug(及修复)
// ❌ Bug:所有处理器封闭了同一个 'i'
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 打印 3, 3, 3 — 而不是 0, 1, 2!
}, 100);
}
// 当定时器运行时,循环已经结束,i === 3
// ✅ 修复 1:使用 let(每次迭代创建新绑定)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 打印 0, 1, 2 ✓
}, 100);
}
// ✅ 修复 2:使用 IIFE 捕获 i
for (var i = 0; i < 3; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI); // 打印 0, 1, 2 ✓
}, 100);
})(i);
}
现代 JavaScript 中的闭包
// React hooks 大量使用闭包
function Counter() {
const [count, setCount] = useState(0);
// handleClick 封闭了 count 和 setCount
const handleClick = () => {
setCount(count + 1); // 使用封闭的 count
};
return <button onClick={handleClick}>{count}</button>;
}
// 陈旧闭包问题(经典 React bug)
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ❌ 陈旧闭包:count 始终是第一次渲染时的 0
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // 空依赖——封闭了初始 count
return <div>{count}</div>;
}
// 修复:使用函数式更新
function GoodCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ✅ 函数式更新:始终使用最新的 count
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
内存注意事项
// ⚠️ 闭包使外部变量保持存活
function createHeavyObject() {
const largeData = new Array(1000000).fill('data'); // 100 万个元素
return function() {
// 只要这个函数存在,largeData 就永远无法被垃圾回收!
return largeData[0];
};
}
// ✅ 限制封闭的内容
function createHeavyObjectFixed() {
const largeData = new Array(1000000).fill('data');
const firstItem = largeData[0]; // 只捕获你需要的内容
return function() {
return firstItem; // largeData 现在可以被垃圾回收了
};
}
总结
- 闭包是一个函数及其捕获的作用域(词法环境)
- 闭包实现了数据隐私、工厂函数和带状态的回调
- JavaScript 中的每个函数都是闭包(它们都捕获了周围的作用域)
- 注意 React hooks 和异步代码中的陈旧闭包
- 内存:闭包使引用的变量保持存活——避免不必要地捕获大对象
→ 使用 String Obfuscator 工具混淆你的 JavaScript 代码。