所属板块:2. 执行上下文与闭包(JS 的核心引擎)
记录日期:2026-03-xx
更新:遇到闭包相关应用或内存问题时补充
1. 闭包的严格定义
闭包(Closure)就是一个函数和它周围状态(词法环境)的引用捆绑在一起的组合。
通俗说:一个函数即使在它定义的环境已经销毁后,依然能访问那个环境里的变量。
更精确的描述(来自 ECMAScript 规范):
一个函数和对其周围词法环境(Lexical Environment)的引用组合在一起,就形成了闭包。
核心特点:函数被当做返回值或参数传递到外部后,依然“记住”了诞生时的作用域链。
2. 闭包的底层原理(连接 [2-1][2-2][2-3])
当外部函数执行完毕、其执行上下文按 [2-1] 所说正常出栈时:
- 正常情况下,变量对象(VO)会被垃圾回收(第一板块记忆机制)
- 但如果内部函数被返回到外部(形成了闭包),JS 引擎会把外部函数的变量对象“劫持”到堆内存,让它继续存活
这就是为什么闭包能“记住”外部变量:
- [2-2] 的作用域链([[outer]] 指针)被保留
- [2-3] 的 this(如果是普通函数)也随之保留
示例(最简闭包):
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const fn = outer(); // outer 执行完,上下文本应销毁
fn(); // 1
fn(); // 2(count 依然存在)
3. 闭包的经典应用场景
-
数据私有化(封装)
function createCounter() { let count = 0; // 外部无法直接访问 return { increment() { count++; return count; }, get() { return count; } }; } const counter = createCounter(); -
防抖与节流(最常用手写题)
function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } -
函数柯里化(Currying)
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) return fn(...args); return (...next) => curried(...args, ...next); }; } -
缓存 / 记忆化(Memoization)
用闭包保存计算结果,避免重复计算。
4. 闭包的副作用:内存泄漏风险
闭包会让外部函数的变量对象无法被 GC 回收。如果闭包长期存活 + 引用了大对象,就容易泄漏。
常见场景:
- 全局变量保存了闭包
- 事件监听器 / 定时器里的闭包没及时清理
解决办法:
let fn = outer(); // 使用完后
fn = null; // 手动断开引用,让 GC 回收
5. 经典高频考题:for + setTimeout + 闭包
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0); // 输出 5 5 5 5 5
}
原因:var i 是函数/全局作用域,所有回调共用同一个 i([2-2] 作用域链)
两种解决方式:
- 用 let(块级作用域,每轮循环都是新闭包)
- 用 IIFE(立即执行函数创建独立作用域)
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(() => console.log(j), 0); })(i); }
6. 小结 & 第二板块完结
- 闭包 = [2-1] 执行上下文 + [2-2] 作用域链 + [2-3] this 的“存活版”
- 用得好是神器(私有化、柯里化、防抖);用不好就是内存泄漏
- 遇到“变量值不对”“内存持续上涨”时,先检查是否有不必要的闭包
第二板块(执行上下文与闭包)到此全部结束。
这是 JS 引擎最核心的“内功心法”,掌握后看框架源码会顺畅很多。
返回总目录:戳这里