小白也能学会 闭包

一、什么是闭包?

闭包 = 函数 + 它能访问的外部变量

换句话说:

闭包就是一个函数能够"记住"并访问它定义时所在的环境中的变量。

听起来有点抽象?没关系,我们先从一个简单的例子开始。


示例 1:一个最简单的闭包

js 复制代码
function outer() {
  const name = "小明";

  function inner() {
    console.log(name); // 使用了外部函数 outer 的变量 name
  }

  return inner;
}

const sayName = outer(); // 这里返回的是 inner 函数本身
sayName(); // 输出 "小明"

分析这个过程:

  1. outer() 函数执行后返回了内部的 inner 函数。
  2. 外部变量 name 是在 outer() 内部定义的。
  3. 即使 outer() 执行完了,它的局部变量 name 并没有被销毁。
  4. sayName() 实际上是 inner() 函数,它仍然能访问到 name 变量。

这就是闭包的核心特性:即使外层函数执行完毕,其内部变量依然可以被内部函数访问和保留。


二、闭包的本质:函数+作用域链

闭包不是一种语法结构,而是一种行为模式。它的本质是:

当一个函数能够访问并记住其词法作用域(也就是它定义时所处的作用域),即使该函数在其作用域外执行,也形成了闭包。


示例 2:闭包让变量"私有化"

js 复制代码
function counter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const increment = counter();

increment(); // 输出 1
increment(); // 输出 2
increment(); // 输出 3

解释:

  • counter() 返回了一个函数,这个函数每次调用都会增加 count
  • countcounter() 中的局部变量,正常情况下函数执行完后应该被销毁。
  • 但由于返回的函数"记住了"这个变量,所以它不会被销毁,而是继续存在内存中。
  • 我们通过闭包实现了对 count 的保护和控制 ------ 这就是 JavaScript 中实现"私有变量"的一种方式!

三、闭包的常见应用场景

闭包非常强大,它在很多地方都有广泛的应用,下面是一些最常见的用途:


数据封装(私有变量)

就像上面的例子一样,我们可以创建一些只能通过特定方法访问的变量。

js 复制代码
function createCounter() {
  let count = 0;

  return {
    increment: function () {
      count++;
    },
    getCount: function () {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出 1
// count 无法直接访问或修改

优点:

  • 避免全局变量污染
  • 提高数据安全性

柯里化(Currying)与函数工厂

闭包可以用来生成定制化的函数。

js 复制代码
function add(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(10)); // 输出 15

这里 add(5) 返回了一个新函数,它"记住"了 x=5,这就是闭包的力量。


回调函数中保存状态

闭包常用于事件处理、异步编程中保存上下文。

js 复制代码
function setupTimer(name) {
  setTimeout(function () {
    console.log("你好," + name);
  }, 1000);
}

setupTimer("张三"); // 1秒后输出 "你好,张三"

在这个例子中,匿名回调函数访问了 setupTimer 中的 name,这其实也是闭包。


循环中绑定事件(经典问题)

这是初学者最容易踩坑的地方:

html 复制代码
<ul>
  <li>项目1</li>
  <li>项目2</li>
  <li>项目3</li>
</ul>
js 复制代码
const items = document.querySelectorAll("li");

for (var i = 0; i < items.length; i++) {
  items[i].addEventListener("click", function () {
    alert("你点击了第 " + i + " 个项目");
  });
}

你会发现无论点击哪个 <li>,弹出的都是"第 3 个项目"。

为什么会这样?

因为 var 是函数作用域,循环结束后 i 的值已经是 3。所有回调函数共享同一个 i

使用闭包解决这个问题:

js 复制代码
for (var i = 0; i < items.length; i++) {
  (function (index) {
    items[i].addEventListener("click", function () {
      alert("你点击了第 " + index + " 个项目");
    });
  })(i);
}

或者更简单的方式(推荐):

js 复制代码
for (let i = 0; i < items.length; i++) {
  items[i].addEventListener("click", function () {
    alert("你点击了第 " + i + " 个项目");
  });
}

let 是块级作用域,每个循环都会创建一个新的 i,相当于自动形成了闭包。


四、闭包的缺点

虽然闭包很强大,但也有一些需要注意的问题:

缺点 说明
内存占用高 闭包会保留外部函数的变量在内存中,可能导致内存泄漏
性能影响 如果滥用闭包,可能会影响程序性能
调试困难 闭包嵌套多层时,调试起来可能比较复杂

五、一句话总结闭包

闭包是一个函数能够访问并记住它定义时所处的作用域中的变量,即使该函数在该作用域之外执行。

也可以简化为:

闭包 = 函数 + 它能访问的外部变量


六、闭包练习题(动手试试看)

练习1:

js 复制代码
function outer() {
  var x = 10;
  return function inner(y) {
    return x + y;
  };
}

var result = outer();
console.log(result(5)); // 输出多少?

练习2:

js 复制代码
function createMultiplier(multiplier) {
  return function (num) {
    return num * multiplier;
  };
}

const double = createMultiplier(2);
console.log(double(5)); // 输出多少?

练习3(经典面试题):

js 复制代码
for (var i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

这段代码会输出什么?如何修改让它依次输出 1、2、3?


七、闭包 vs 其他概念对比

概念 特点
闭包 函数 + 外部变量,保持变量不被销毁
作用域 控制变量的可访问范围
this 动态绑定,取决于函数调用方式
箭头函数 没有自己的 this,继承外层 this
模块模式 利用闭包实现模块封装、暴露接口

八、结语

闭包是 JavaScript 中非常核心、也非常强大的概念。虽然刚开始可能会觉得有点绕,但只要你理解了它的原理和使用场景,就能写出更优雅、更高效的代码。

闭包就像是你的函数随身带着一个小背包,里面装着它出生时周围的变量。

如果你现在看完这篇文章,已经能回答这些问题:

  • 什么是闭包?
  • 为什么需要闭包?
  • 闭包有什么好处和坏处?
  • 常见的闭包写法有哪些?
  • 如何避免闭包带来的问题?

那么恭喜你,你已经掌握了 JavaScript 中最重要的概念之一!

相关推荐
熊的猫22 分钟前
校招生问我在vue中,什么时候该用 render 函数?
前端·javascript·vue.js
小蜜蜂嗡嗡3 小时前
【flutter对屏幕底部有手势区域(如:一条横杠)导致出现重叠遮挡】
前端·javascript·flutter
伍哥的传说4 小时前
Vue 3 useModel vs defineModel:选择正确的双向绑定方案
前端·javascript·vue.js·definemodel对比·usemodel教程·vue3.4新特性·vue双向绑定
胡gh9 小时前
页面卡成PPT?重排重绘惹的祸!依旧性能优化
前端·javascript·面试
胡gh10 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
言兴10 小时前
# 深度解析 ECharts:从零到一构建企业级数据可视化看板
前端·javascript·面试
烛阴11 小时前
TypeScript 的“读心术”:让类型在代码中“流动”起来
前端·javascript·typescript
silent_missile12 小时前
element-plus穿梭框transfer的调整
前端·javascript·vue.js
山有木兮木有枝_14 小时前
node文章生成器
javascript·node.js
yes or ok15 小时前
前端工程师面试题-vue
前端·javascript·vue.js