闭包是 JavaScript 里一个函数能够记住并访问它定义时所在作用域的现象。JavaScript 闭包之所以让很多初学者绕晕,是因为它把作用域链悄悄藏在了函数内部,你看不见却一直在起作用。当你嵌套定义一个函数、并且内层函数用到了外层函数的变量,闭包就产生了。今天这篇文章,编程狮就用最通俗的方式把它讲透:它是什么、为什么能记住变量、以及最常见的坑在哪里,争取让你读完就能在代码里认出它。闭包不是 ES6 才有的概念,从 JavaScript 早期版本起它就存在,只是过去常被忽略,直到前端工程化之后才变成面试和实战的高频考点。
一、先用一句大白话定义闭包
闭包,就是一个函数,加上它定义时所能看到的那一层外层作用域,打包在一起后的整体。你可以把它想象成「函数随身带了一个小背包」,背包里装着它被创建时周围的所有变量。JavaScript 的作用域分为词法作用域,闭包正是词法作用域的自然产物,它让函数嵌套时内层能一直看到外层。
function outer() {
let name = "编程狮";
function inner() {
console.log(name); // 这里用到了外层的 name
}
return inner;
}
const fn = outer();
fn(); // 输出:编程狮
上面这段里,inner 用到了外层 outer 的 name 变量,并且 outer 把 inner 返回了出去。要读懂这段,关键是 JavaScript 闭包把 name 所在的作用域一起打包带走了。哪怕 outer 已经执行完,fn() 再次调用时依然能拿到 name,这就是 JavaScript 闭包最经典的样子:函数带着它的作用域一起活了下来。
💡 小提示:如果你对函数和变量的基础还生疏,先过一遍 JavaScript 教程 把作用域这部分补上,再看闭包会轻松很多。
二、为什么闭包能「记住」外部变量
要理解 JavaScript 闭包为什么能记住变量,得先知道作用域链。每次函数被创建,它都会记住自己是在哪一层作用域里出生的,这个「出身记录」就叫作用域链。理解 JavaScript 闭包的关键,就在于看清这条链。为了把这件事说透,看个对比:词法作用域意味着函数「出生在哪」就决定了它能看到谁,和「在哪里被调用」无关。所以一个闭包,无论被传到哪里去调用,它记住的永远是定义时那层外层变量。
当你在一个函数里访问某个变量时,JavaScript 会顺着作用域链一层层往上找:先在自己内部找,找不到就去外层,再找不到再去更外层,直到全局作用域。闭包之所以特殊,是因为即使外层函数已经执行结束、本该被回收,只要内层函数还活着,那条作用域链就不会断,外层变量就被「保活」了。
在日常开发中,这种「记忆能力」正是许多封装技巧的底层支撑。比如一个事件处理器只要在定义时捕获了某个变量,之后无论页面怎么变化,它都能稳定拿到当初那个值,所以闭包才会在各类库和框架里被广泛使用。
关于语法的细节点和更多示例,可以翻 JavaScript 速查手册 随时查,它把常见的语法陷阱都列了出来。
三、闭包到底有什么用
光说概念太虚,JavaScript 闭包在实战里有三个最常见的作用。很多人第一次见到 JavaScript 闭包,就是在「私有变量」这个场景。
第一个是「私有变量」。你可以返回一组函数,它们共享同一个被闭包保护的变量,外部却无法直接改它:
function createCounter() {
let count = 0;
return {
inc() { count += 1; return count; },
get() { return count; }
};
}
const c = createCounter();
c.inc(); // 1
c.inc(); // 2
上面这段里,count 被闭包包住,外面拿不到,只能通过 inc 和 get 这两个函数去碰它,这就实现了简单的私有状态。
作为补充,再看一个工厂函数的例子:
function makeAdder(n) {
return function (x) { return x + n; };
}
const add5 = makeAdder(5);
console.log(add5(10)); // 15
上面这段里,makeAdder 返回的函数记住了参数 n,于是 add5 天然带着「加 5」的能力,这就是闭包在工厂模式里的典型用法。
第二个是工厂函数,根据参数生成定制化的函数;第三个是回调函数里保存现场,比如给一组按钮绑定不同的值。想深入作用域本身,这篇 JavaScript作用域:理解代码的作用范围 可以作为补充阅读,它把词法作用域讲得更细,和闭包正好是同一件事的两个侧面。
四、最容易踩的坑:循环里的 var
JavaScript 闭包最常见的坑,出现在循环里用 var 绑定事件:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出 3 3 3,不是 0 1 2
}
上面这段会输出三个 3,因为 var 声明的 i 在整个循环里是同一个变量,等定时器触发时循环早跑完了,i 已经是 3。闭包记住的是「变量本身」,不是它被创建那一刻的值。
为什么 let 能解决?因为 let 在循环里每次迭代都会创建全新的作用域,每次定时器捕获的是当次迭代独立的 i,自然就是 0、1、2。如果把 var 那个例子想成「所有人共用一本笔记」,let 就是「每人发一本自己的笔记」,闭包记住的也就不再混淆。
解决办法是把 var 换成 let,let 在每次循环都会创建一个独立的作用域;或者手写一个闭包把当前值存进参数。关于原型链和闭包的关系,JavaScript 原型教程 也能帮你把对象模型理清楚,函数嵌套和原型链其实是同一个作用域体系下的两件事。
总结
JavaScript 闭包并不神秘,它就是「函数 + 它出生时的作用域」的组合体。因为作用域链被一起打包带走,所以内层函数即使在外层函数结束后,依然能访问当时的变量。
要点带走:
- 闭包 = 函数 + 定义时的外层作用域;
- 它能记住变量,靠的是不会被断开的作用域链;
- 私有变量、工厂函数、回调保存现场,是三大实战用途;
- 循环里用
var是经典坑,换成let就能解决。
下一步,把作用域链和原型链放在一起理解,你对 JavaScript 的对象模型会有更完整的认识。
延伸学习
想系统补齐前端基础,可以按这个顺序来:
- 先过一遍 AI×Cursor前端开发课程,从 HTML、CSS、JavaScript 一路学到高级项目实战;
- 想理解函数怎么在 Node 环境里跑起来,Node.js 教程 能带你把运行环境搭起来。
常见问题
Q:闭包会不会造成内存泄漏?
A:不一定。闭包确实会让外层变量一直不被回收,但只要那个函数不再被引用,相关作用域最终还是会被释放。真正要小心的,是把闭包挂到全局、或者塞进长期存活的对象里,导致本该释放的变量迟迟不走。
Q:箭头函数也有闭包吗?
A:有。箭头函数同样会捕获定义时的作用域,它和普函函数一样能形成闭包,区别主要在 this 的绑定规则上,和能不能形成闭包无关。
Q:怎么判断一段代码里到底有没有闭包?
A:只要一个内层函数用到了外层函数的变量,并且这个内层函数被带到了外层函数之外(返回、传参、存全局),闭包就成立了。只看「内层用了外层变量」这一条就够判断。

免费 AI IDE



