深入理解 JavaScript 闭包
什么是闭包?
闭包(Closure)是 JavaScript 中最核心也最常被问到的概念之一。简单来说,闭包是指一个函数能够记住并访问其词法作用域(Lexical Scope)中的变量,即使这个函数在其词法作用域之外执行。
function outer() {
const message = 'Hello from closure!';
function inner() {
console.log(message);
}
return inner;
}
const fn = outer();
fn(); // 输出: Hello from closure!上面的 inner 函数就是一个闭包——它”捕获”了 message 变量,即使在 outer 执行完毕后仍然可以访问它。
闭包的工作原理
词法作用域
JavaScript 采用词法作用域(也称静态作用域),这意味着函数的作用域在定义时就已经确定,而不是在调用时。
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(globalVar); // 可访问
console.log(outerVar); // 可访问
console.log(innerVar); // 可访问
}
inner();
}
outer();作用域链
每个函数在定义时都会创建一个作用域链(Scope Chain),它包含了当前函数的所有外层作用域的引用。当函数执行并访问变量时,JavaScript 引擎会沿着这条链逐层查找。
执行上下文与词法环境
当函数执行时,会创建一个执行上下文(Execution Context),其中包含:
- 变量环境(Variable Environment):存储
let、const声明的变量 - 词法环境(Lexical Environment):存储函数声明和
var声明的变量 - 外部环境引用(Outer Environment Reference):指向外层词法环境的引用
闭包正是通过这个”外部环境引用”来访问外层作用域的变量。
闭包的常见应用场景
1. 数据私有化
闭包可以用来创建私有变量,这是模块模式的基础:
function createCounter() {
let count = 0; // 私有变量
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// 无法直接访问 count 变量2. 函数工厂
闭包可以用来创建带有预设参数的函数:
function multiply(factor) {
return function(number) {
return number * factor;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 153. 事件处理与回调
在异步编程中,闭包帮助我们保存状态:
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener('click', function() {
// 这个回调函数是一个闭包,捕获了 message 变量
alert(message);
});
}
setupButton('btn1', '按钮一被点击');
setupButton('btn2', '按钮二被点击');4. 循环中的闭包
经典的面试题——循环中的闭包问题:
// 问题:使用 var 时,所有回调都输出 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 输出 5 次 5
}, 100);
}
// 解法 1:使用 IIFE 创建独立作用域
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2, 3, 4
}, 100);
})(i);
}
// 解法 2:使用 let(推荐)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2, 3, 4
}, 100);
}闭包与内存管理
内存泄漏风险
闭包会持有对外部变量的引用,如果不小心使用,可能导致内存泄漏:
function createLargeData() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log('closure still holds largeData');
// 即使这里没有使用 largeData,它也无法被 GC 回收
};
}
const fn = createLargeData();
// largeData 仍然存在于内存中最佳实践
- 及时释放:不再需要的闭包,将引用设为
null - 只捕获需要的变量:避免捕获整个大对象
- 使用 WeakMap/WeakSet:对于需要缓存但不想阻止 GC 的场景
function createProcessor(data) {
// 只捕获需要的部分
const id = data.id;
return {
getId: () => id,
process: () => {
// 使用 data 进行处理
return data.value * 2;
}
};
}闭包在框架中的应用
React Hooks
React 的 useEffect、useCallback 等 Hook 都依赖闭包来捕获状态:
function Counter() {
const [count, setCount] = useState(0);
// 这个 effect 捕获了 count 变量
useEffect(() => {
document.title = `点击了 ${count} 次`;
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}Vue Composition API
Vue 3 的 setup 函数同样利用闭包:
import { ref, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
// increment 是一个闭包,捕获了 count
function increment() {
count.value++;
}
onMounted(() => {
console.log('组件已挂载');
});
return { count, increment };
}
};总结
闭包是 JavaScript 中一个强大且基础的特性。理解闭包的关键在于:
- 词法作用域:函数的作用域在定义时确定
- 作用域链:函数可以访问其外层作用域的变量
- 变量持有:闭包让变量在函数执行完毕后仍然存活
掌握闭包不仅能帮你写出更优雅的代码,还能让你更深入地理解 JavaScript 的运行机制。它是通往高级 JavaScript 开发的必经之路。