别再说闭包难懂了,它只是太深情 “一旦理解,你会爱上这个‘记性好’的家伙。”

465 阅读9分钟

闭包(Closure)是 JavaScript 中最具魅力的语言特性之一。
它不仅支撑了高阶函数、模块化、上下文绑定等高级编程技巧,更是前端开发中性能优化、状态管理、封装设计的核心工具。本文将从闭包的基本概念讲起,深入剖析其底层机制,并结合六个真实场景进行详细讲解,让你看完后能够:

✅ 理解闭包的本质
✅ 分清闭包和作用域链的关系
✅ 掌握 this 的绑定机制
✅ 看懂并写出防抖、节流、记忆函数等常见闭包应用
✅ 在实际项目中灵活运用闭包解决问题


📚 目录

  1. 什么是闭包?
  2. 闭包的本质是什么?它为什么存在?
  3. 闭包是如何形成的?执行上下文的作用
  4. 闭包的三大核心特点
  5. 闭包的六大经典应用场景详解
    • 防抖(Debounce)
    • 节流(Throttle)
    • 上下文绑定(Context Binding)
    • 事件监听器中的闭包(含完整 HTML 示例)
    • 记忆函数(Memoization)
    • 模块封装(IIFE)(含完整 HTML 示例)
  6. 关于 this 的深度剖析
  7. 闭包的注意事项与性能优化
  8. 总结:闭包到底是什么?我们为什么要用它?

一、什么是闭包?

✅ 最简单的定义:

闭包是一个函数能够访问并记住它的词法作用域,即使该函数在其作用域外执行。

🔍 举个例子:

function outer() {
  let count = 0;
  return function inner() {
    console.log(++count);
  };
}

const counter = outer();
counter(); // 输出 1
counter(); // 输出 2

在这个例子中,inner() 函数就是一个闭包。它虽然在 outer() 外部被调用,但依然可以访问 outer() 内部的变量 count


二、闭包的本质是什么?它为什么存在?

🧩 我们先来想一个问题:

如果没有闭包,函数只能使用全局变量或参数传递数据。那如果我需要一个函数记住一些状态,又不想让这个状态暴露给全局呢?

这就是闭包存在的意义:为函数提供私有状态,形成独立的作用域空间。

📌 闭包的本质是:

一个函数 + 它创建时所处的词法作用域环境的组合体。

你可以把它想象成一个“盒子”,里面装着函数本身和它能访问的所有变量。


三、闭包是如何形成的?执行上下文的作用

🧠 执行上下文(Execution Context)

每次函数被调用时,都会创建一个执行上下文。它包括:

  • 变量对象(Variable Object):存储函数内部声明的变量、函数、参数等
  • 作用域链(Scope Chain):决定当前函数能访问哪些变量
  • this

📌 举个例子说明闭包的形成过程:

function createCounter() {
  let count = 0;
  return function () {
    return ++count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

执行流程如下:

  1. createCounter() 被调用 → 创建执行上下文 → count = 0
  2. 返回了一个匿名函数,它引用了 count
  3. 即使 createCounter() 已经执行完毕,由于返回的函数仍然持有对 count 的引用,所以它不会被垃圾回收。
  4. 每次调用 counter(),都会修改 count,因为它存在于闭包环境中。

四、闭包的三大核心特点

特点描述
保持变量不被销毁即使函数已经执行完毕,只要还有函数引用了它,就不会被垃圾回收
形成私有作用域变量只对外部开放的函数可见,避免污染全局
实现状态封装可以用来保存状态,实现类似类的私有属性

五、闭包的六大经典应用场景详解


1️⃣ 防抖(Debounce)

📌 场景描述:

防止用户频繁触发某个操作,比如输入框搜索建议、窗口大小调整等。

💡 示例代码:

function debounce(fn, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn.apply(this, args), delay);
  };
}

🧾 应用示例:

input.addEventListener('input', debounce((e) => {
  console.log('发送请求:', e.target.value);
}, 300));

🔍 核心机制:

  • 每次触发都清除之前的定时器
  • 只有当最后一次触发后经过一定时间不再触发,才真正执行回调

📌 运行结果说明:

当你在输入框快速连续输入时,例如输入 abc,控制台不会立即打印三次,而是等待300ms内没有新的输入后,才输出最终值 "发送请求: abc"


2️⃣ 节流(Throttle)

📌 场景描述:

控制函数执行频率,确保单位时间内只执行一次,如滚动加载、动画帧控制。

💡 示例代码:

function throttle(fn, limit) {
  let lastRan = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastRan >= limit) {
      fn.apply(this, args);
      lastRan = now;
    }
  };
}

🧾 应用示例:

window.addEventListener('resize', throttle(() => {
  console.log('窗口大小改变');
}, 1000));

🔍 核心机制:

  • 判断上次执行时间和当前时间间隔是否满足条件
  • 满足则执行,否则跳过

📌 运行结果说明:

当你拖动浏览器边框调整窗口大小时,每秒钟只会输出一次 "窗口大小改变",即使你持续调整窗口。


3️⃣ 上下文绑定(Context Binding)

📌 场景描述:

解决 this 指向错误的问题,特别是在异步回调中。

❗ 问题示例:

const obj = {
  name: 'closure',
  sayName: function () {
    setTimeout(function () {
      console.log(this.name); // undefined
    }, 1000);
  }
};
obj.sayName();

✅ 解决方案:

  • 使用箭头函数(推荐)
setTimeout(() => {
  console.log(this.name); // closure
}, 1000);
  • 使用 .bind(this)
setTimeout(function () {
  console.log(this.name);
}.bind(this), 1000);
  • 使用中间变量 that = this
const that = this;
setTimeout(function () {
  console.log(that.name);
}, 1000);

📌 运行结果说明:

如果不使用闭包或其他方式绑定 thisthis.name 会是 undefined(因为 setTimeout 中的 this 指向全局)。使用上述任意一种方法后,都能正确输出 "closure"


4️⃣ 事件监听器中的闭包

📌 场景描述:

在事件处理函数中保留状态而不污染全局变量。

💡 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>闭包中的事件监听</title>
</head>
<body>
  <button id="myButton">Click Me</button>
  <script>
    const obj = {
      message: "Hello from object",
      init: function(){
        const button = document.getElementById('myButton');
        const that = this; 
        button.addEventListener('click', function(){
          console.log(that.message);
        });
      }
    };

    obj.init();
  </script>
</body>
</html>

🔍 代码解析:

这段代码展示了如何在事件监听中利用闭包访问外部对象的状态:

  • obj 是一个包含 message 属性和 init 方法的对象。
  • init 方法获取按钮并为其添加点击事件监听器。
  • 在监听器函数中,通过 that.message 来访问 obj.message
  • 这里之所以能访问到 obj.message,是因为 that 是一个自由变量,被监听器函数捕获形成了闭包。

📌 关键点:

  • 使用 that = this 缓存 this 的值,以便在事件回调中访问原始对象的属性。
  • 事件回调函数形成了一个闭包,保留了对外部作用域变量的引用。

📌 运行结果说明:

当你点击页面上的按钮后,控制台会输出 "Hello from object",这说明闭包成功保留了对 obj.message 的引用。


5️⃣ 记忆函数(Memoization)

📌 场景描述:

缓存函数执行结果,提高性能,适用于递归、复杂计算等场景。

💡 示例代码:

function memoize(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    if (key in cache) return cache[key];
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

🧾 应用示例:

function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

const memoizedFactorial = memoize(factorial);
memoizedFactorial(5); // 第一次计算
memoizedFactorial(5); // 缓存命中

🔍 核心机制:

  • 使用对象作为缓存池
  • 将参数序列化为字符串作为键
  • 第一次计算后缓存结果,后续直接读取

📌 运行结果说明:

第一次调用 memoizedFactorial(5) 会进行计算,返回 120;第二次调用相同参数时,函数会直接从缓存中取出结果,而不会再次计算,从而提升性能。


6️⃣ 模块封装(IIFE)

📌 场景描述:

使用立即执行函数表达式(IIFE)创建私有作用域,实现模块封装。

💡 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>立即执行函数 IIFE</title>
</head>
<body>
  <script>
    const Counter = (function () {
      let count = 0; // 私有变量
      function increment() {
        return ++count;
      }
      function reset() {
        count = 0;
      }

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

    const counter1 = Counter(); 
    const counter2 = Counter(); 

    console.log(counter1.getCount()); // 0
    counter1.increment(); 
    console.log(counter2.getCount()); // 1
  </script>
</body>
</html>

🔍 代码解析:

这段代码展示了一个基于 IIFE 的计数器模块封装:

  • Counter 是一个由 IIFE 返回的工厂函数。
  • 在 IIFE 内部,定义了私有变量 count 和两个操作方法 incrementreset
  • 工厂函数返回一个新的对象,提供 getCountincrementreset 三个公开方法。
  • counter1counter2 是通过调用 Counter() 创建的实例。

📌 关键点:

  • count 是闭包变量,被多个实例共享,因此它们的操作会互相影响。
  • 若希望每个实例拥有独立的 count,应在工厂函数中重新定义 count,而不是共享 IIFE 中的变量。

📌 运行结果说明:

counter1.getCount() 初始返回 0,调用 counter1.increment()count 增加为 1。接着调用 counter2.getCount(),发现也返回 1,说明两个实例共享同一个 count 变量。


六、关于 this 的深度剖析

📌 this 是什么?

this 是 JavaScript 中一个特殊的运行时绑定,它的值取决于函数的调用方式,而不是定义方式。

🧩 四种常见的绑定规则:

绑定方式描述示例
默认绑定非严格模式下指向 window,严格模式下为 undefinedfn()
隐式绑定方法调用,this 指向调用者obj.method()
显式绑定使用 call/apply/bind 显式指定 thisfn.call(obj)
new 绑定构造函数调用,this 指向新对象new Person()

🎯 箭头函数中的 this

箭头函数没有自己的 this,它的 this 是继承自外层作用域的 this

const obj = {
  name: 'closure',
  sayName: () => {
    console.log(this.name); // window.name 或 undefined
  }
};

❌ 错误:箭头函数不能用于对象方法中绑定 this
✅ 正确:普通函数或使用 .bind(this) 更合适


七、闭包的注意事项与性能优化

⚠️ 注意事项:

  • 内存泄漏风险:闭包会阻止垃圾回收,应手动清理不需要的状态
  • 调试困难:闭包变量不容易查看,调试时需注意作用域链
  • 影响性能:大量闭包可能导致内存占用过高

✅ 优化建议:

  • 使用完闭包后,及时解除引用(如赋值为 null
  • 避免在循环中创建过多闭包
  • 对于高频函数可使用 requestIdleCallback 替代 setTimeout

八、总结:闭包到底是什么?我们为什么要用它?

闭包 = 函数 + 作用域链 + 私有状态管理

✅ 闭包的核心价值:

  • 提供函数私有状态,避免全局污染
  • 实现模块封装,构建健壮的组件
  • 支撑防抖、节流、记忆函数等实用功能
  • 是现代前端框架(React、Vue)中状态管理的基础

🧠 学习闭包的关键思路:

  1. 从作用域链理解闭包的形成
  2. 通过执行上下文分析闭包生命周期
  3. 结合实际案例理解闭包的应用场景
  4. 对比不同写法,理解闭包的优劣
  5. 最后回到语言本质,理解闭包为何存在

📖 一句话总结闭包:

闭包是 JavaScript 中最优雅的设计之一,它让我们可以在函数之外,安全地保存状态、隔离作用域、控制行为。