什么是闭包?一文讲透 JavaScript 里最易绕晕的那个概念

编程狮(w3cschool.cn) 2026-08-12 15:21:52 浏览数 (40)
反馈

闭包是 JavaScript 里一个函数能够记住并访问它定义时所在作用域的现象。JavaScript 闭包之所以让很多初学者绕晕,是因为它把作用域链悄悄藏在了函数内部,你看不见却一直在起作用。当你嵌套定义一个函数、并且内层函数用到了外层函数的变量,闭包就产生了。今天这篇文章,编程狮就用最通俗的方式把它讲透:它是什么、为什么能记住变量、以及最常见的坑在哪里,争取让你读完就能在代码里认出它。闭包不是 ES6 才有的概念,从 JavaScript 早期版本起它就存在,只是过去常被忽略,直到前端工程化之后才变成面试和实战的高频考点。

一、先用一句大白话定义闭包

闭包,就是一个函数,加上它定义时所能看到的那一层外层作用域,打包在一起后的整体。你可以把它想象成「函数随身带了一个小背包」,背包里装着它被创建时周围的所有变量。JavaScript 的作用域分为词法作用域,闭包正是词法作用域的自然产物,它让函数嵌套时内层能一直看到外层。

function outer() {
  let name = "编程狮";
  function inner() {
    console.log(name);  // 这里用到了外层的 name
  }
  return inner;
}
const fn = outer();
fn();  // 输出:编程狮

上面这段里,inner 用到了外层 outername 变量,并且 outerinner 返回了出去。要读懂这段,关键是 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 被闭包包住,外面拿不到,只能通过 incget 这两个函数去碰它,这就实现了简单的私有状态。

作为补充,再看一个工厂函数的例子:

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 换成 letlet 在每次循环都会创建一个独立的作用域;或者手写一个闭包把当前值存进参数。关于原型链和闭包的关系,JavaScript 原型教程 也能帮你把对象模型理清楚,函数嵌套和原型链其实是同一个作用域体系下的两件事。

总结

JavaScript 闭包并不神秘,它就是「函数 + 它出生时的作用域」的组合体。因为作用域链被一起打包带走,所以内层函数即使在外层函数结束后,依然能访问当时的变量。

要点带走:

  • 闭包 = 函数 + 定义时的外层作用域;
  • 它能记住变量,靠的是不会被断开的作用域链;
  • 私有变量、工厂函数、回调保存现场,是三大实战用途;
  • 循环里用 var 是经典坑,换成 let 就能解决。

下一步,把作用域链和原型链放在一起理解,你对 JavaScript 的对象模型会有更完整的认识。

延伸学习

想系统补齐前端基础,可以按这个顺序来:

  1. 先过一遍 AI×Cursor前端开发课程,从 HTML、CSS、JavaScript 一路学到高级项目实战;
  2. 想理解函数怎么在 Node 环境里跑起来,Node.js 教程 能带你把运行环境搭起来。

常见问题

Q:闭包会不会造成内存泄漏?
A:不一定。闭包确实会让外层变量一直不被回收,但只要那个函数不再被引用,相关作用域最终还是会被释放。真正要小心的,是把闭包挂到全局、或者塞进长期存活的对象里,导致本该释放的变量迟迟不走。

Q:箭头函数也有闭包吗?
A:有。箭头函数同样会捕获定义时的作用域,它和普函函数一样能形成闭包,区别主要在 this 的绑定规则上,和能不能形成闭包无关。

Q:怎么判断一段代码里到底有没有闭包?
A:只要一个内层函数用到了外层函数的变量,并且这个内层函数被带到了外层函数之外(返回、传参、存全局),闭包就成立了。只看「内层用了外层变量」这一条就够判断。

0 人点赞