闭包是函数引用外层变量且该函数在外部作用域结束后仍存活时自动形成的机制;判断需同时满足:外部定义变量、内部显式访问、内部函数被带出作用域。

闭包不是“学了才会用”的东西,而是你写 JavaScript 时已经踩进去的坑或用上的工具——只要函数引用了外层变量,并且这个函数在外部作用域结束后还活着,闭包就自动形成了。
闭包怎么一眼认出来?看三个运行时条件
很多人以为 return function 就是闭包,其实关键不在写法,而在运行时行为:
- 外部函数定义了变量(推荐用
let或const,避免var提升干扰判断) - 内部函数显式访问了那个变量(哪怕只是
console.log(x)) - 这个内部函数被“带出”了外部作用域(比如被
return、赋值给全局变量、传给addEventListener)
缺一不可。下面这段代码看似像闭包,但实际没有:
function outer() {
let x = 10;
function inner() {
console.log('hello'); // 没引用 x
}
return inner;
}
const f = outer();
f(); // x 会被正常回收,不构成闭包
作用域链不是概念,是变量查找的真实路径
JavaScript 在函数定义时就锁定了它的作用域链,执行时按顺序逐层向上找:当前作用域 → 外层函数作用域 → 全局作用域。闭包之所以能“记住”外部变量,正是因为内部函数的作用域链里始终保留着对外部变量对象的引用。
立即学习“Java免费学习笔记(深入)”;
这意味着:createCounter 执行完后,count 没被回收,不是因为“闭包特殊”,而是因为垃圾回收器发现它 still has a reference —— 被返回的函数持有着。
常见误解:以为作用域链是“动态构建”的。错。它是静态的、定义时确定的。这也是为什么下面这段代码输出是 10 而不是 20:
function makeAdder(x) {
return function(y) { return x + y; };
}
const add10 = makeAdder(10);
console.log(add10(0)); // 10 —— x 是定义时捕获的 10,不是调用时的任何值
哪些场景真正在用闭包?别只盯着计数器
闭包最实在的用途,是封装状态、隔离数据、延迟绑定。它不是炫技,而是解决具体问题的自然选择:
-
私有变量模拟:比
#privateField兼容性更好,适合需要支持旧环境的模块(如 UMD 包) -
事件监听中的稳定上下文:比如循环中给按钮绑定点击,用闭包保存每次迭代的
i值(现代可用let,但本质仍是闭包机制在支撑) -
防抖/节流函数:
debounce内部必须持有定时器 ID 和上一次参数,这些都靠闭包维持 -
API 封装与配置预置:比如
const apiV1 = createApi('https://v1.example.com'),后续所有请求都自动带上 base URL
注意一个高频错误:return { count } 是无效封装 —— 它只是拷贝当前值,无法维持引用;必须返回函数,让它们持续访问同一个 count 变量。
闭包最大的代价不是难懂,而是内存滞留
闭包本身没性能问题,但一旦引用了大对象(比如 DOM 节点、大型数组、未清理的事件监听器),而该闭包又长期存活(比如挂到全局、绑定到长生命周期组件),就会导致内存无法释放。
排查建议:
- 用 Chrome DevTools 的
Memory > Take heap snapshot对比前后,筛选 “Closure” 类型对象 - 检查是否无意中把
this、arguments或整个event对象闭包进了回调 - 在不需要时主动解除引用,比如
innerRef = null或移除事件监听器
真正容易被忽略的,不是“怎么写闭包”,而是“什么时候该放手”——当一个闭包不再需要访问外部变量时,及时切断引用,比优化写法更重要。









