一、开篇直击:为什么闭包是 JS 的 "灵魂知识点"?
你是否遇到过这些场景:
- 想在函数外部访问函数内部变量,却被告知 "ReferenceError"?
- React Hooks 中,useEffect 捕获状态后为何不会随渲染更新?
- 循环绑定事件时,点击元素总是拿到最后一个值?
这些问题的答案,都指向 JavaScript 的核心特性 ------闭包(Closure)。它不是语法糖,而是 JS 词法作用域与函数一等公民特性共同催生的 "自然产物",更是模块化、高阶函数、状态封装的底层支撑。掌握闭包,才算真正入门 JS 的 "内功心法"。
二、闭包的本质:3 分钟看懂底层逻辑
1. 先明确两个前提
- 词法作用域:函数的作用域由定义时的位置决定,而非调用时(比如函数 A 嵌套在函数 B 中,A 能访问 B 的变量,无论 A 在哪里调用)。
- 函数一等公民:函数可作为参数传递、返回值返回,且在调用时会创建独立的执行上下文。
2. 闭包的定义(精准版)
当一个内部函数被其外部作用域之外的变量引用时,就形成了闭包。此时内部函数会 "捕获" 其定义时所在的作用域链,即使外部函数执行完毕,作用域链中的变量也不会被垃圾回收(GC),仍能被内部函数访问。
3. 可视化案例:闭包的形成过程
function createCounter() {
let count = 0; // 外部函数的局部变量
return function increment() { // 内部函数(被外部引用)
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
底层逻辑拆解:
- createCounter 执行时,创建独立执行上下文(EC),其中包含 count 变量。
- 执行完毕后,正常情况下 EC 会被销毁,但由于 increment 函数(内部函数)被外部变量 counter 引用,且 increment 依赖 count,JS 引擎会保留 createCounter 的作用域链。
- 每次调用 counter()(即 increment()),都会通过闭包访问到最初的 count 变量,实现状态持久化。
三、闭包的核心应用场景(实战为王)
1. 模块化封装:实现 "私有变量"
JS 原生没有私有变量,但闭包可模拟:
const module = (function() {
let privateVar = "我是私有变量"; // 外部无法直接访问
return {
getPrivateVar: function() {
return privateVar; // 仅通过暴露的方法访问
},
setPrivateVar: function(val) {
privateVar = val; // 可控修改
}
};
})();
console.log(module.privateVar); // undefined(私有)
console.log(module.getPrivateVar()); // "我是私有变量"
module.setPrivateVar("修改后的私有变量");
实际价值:Vue2 的响应式模块、jQuery 的源码中,大量使用闭包实现模块化,避免全局变量污染。
2. 状态持久化:高阶函数与 Hooks
- 高阶函数示例(防抖节流的核心):
function debounce(fn, delay) {
let timer = null; // 闭包保存定时器状态
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const debouncedClick = debounce(() => console.log("点击"), 1000);
debouncedClick(); // 多次点击仅最后一次生效
- React Hooks 底层:useState useEffect 本质是闭包捕获组件渲染时的状态和上下文,这也是为什么 Hooks 不能在条件语句中使用(会破坏闭包的作用域链)。
3. 循环中的异步处理:解决经典问题
// 反例:循环绑定事件,点击全是最后一个值
for (var i = 0; i {
document.getElementById(`btn${i}`).onclick = function() {
console.log(i); // 点击所有按钮都输出3
};
}
// 正例:用闭包捕获每次循环的i
for (var i = 0; i {
(function(j) { // 立即执行函数创建闭包
document.getElementById(`btn${j}`).onclick = function() {
console.log(j); // 正确输出0、1、2
};
})(i);
}
延伸:ES6 的 let 块级作用域本质也是通过闭包实现的,上述问题用 let i 可直接解决。
四、闭包的 "坑":内存泄漏与性能优化
1. 内存泄漏的原因
闭包会阻止外部函数的作用域被回收,如果闭包被长期引用(比如挂载在 window 上),且作用域中包含大量数据(如 DOM 元素、大对象),会导致内存无法释放,最终引发内存泄漏。
2. 常见泄漏场景与解决方案
|----------------------------------------------|------------------------------|
| 泄漏场景 | 解决方案 |
| 闭包中引用 DOM 元素,元素已被移除但闭包仍存在 | 手动解除引用:elem = null |
| 全局变量引用闭包(如 window.counter = createCounter()) | 不需要时销毁:window.counter = null |
| 定时器 / 事件监听器中使用闭包,未清除 | 组件卸载时清除定时器 / 解绑事件 |
3. 优化原则
- 只在必要时使用闭包(避免过度封装);
- 闭包中尽量只捕获必要的变量(减少作用域链长度);
- 短期使用的闭包,使用后手动解除引用。
五、面试高频考点:闭包相关真题解析
真题 1:说出以下代码的输出结果
function outer() {
let x = 10;
function inner() {
console.log(x);
}
x = 20;
return inner;
}
const fn = outer();
fn(); // 输出20(闭包捕获的是变量引用,而非值)
关键思路:闭包捕获的是变量的 "引用",而非创建时的固定值,所以后续修改外部变量会影响闭包的访问结果。
真题 2:闭包与作用域链的关系
答案核心:闭包的本质是作用域链的延长 ------ 内部函数被外部引用后,其作用域链不会随外部函数执行完毕而销毁,而是被保留下来,供内部函数后续访问。
六、总结:闭包的 "道" 与 "术"
- 道:闭包是 JS 词法作用域的自然延伸,是 "函数一等公民" 特性的必然结果;
- 术:用闭包实现模块化、状态持久化、异步处理,但需警惕内存泄漏;
- 终极认知:闭包不是 "技巧",而是 JS 的底层机制 ------ 理解闭包,才能真正看懂框架源码、写出高性能代码。
掌握闭包后,你会发现:原来 Vue 的响应式、React 的 Hooks、jQuery 的封装,都只是闭包的 "应用场景" 而已。