JavaScript 闭包:函数背后的“背包”(一文详解)

 更新时间:2026年05月25日 09:17:58   作者:yqcoder  
文章详细解释了JavaScript闭包的概念及其作用,包括数据封装、函数柯里化和异步回调中的的状态保持,并通过实例展示了闭闭如何工作以及如何避免潜在的内存泄漏问题,感兴趣的朋友一起看看吧

🤔 什么是闭包?

官方定义
闭包是指有权访问另一个函数作用域中变量的函数

通俗解释
想象一个函数是一个,他出生时背了一个背包(Scope/作用域)。
这个背包里装着他出生时周围环境中的所有变量。
即使这个人走到了世界的尽头(全局环境),或者他的父母(外部函数)已经去世(执行完毕),他依然背着那个背包,可以随时拿出里面的东西使用。

一句话总结
闭包 = 函数 + 函数能访问到的自由变量(背包里的东西)。

1. 🎒 核心原理:为什么会有闭包?

要理解闭包,首先要理解 JS 的词法作用域(Lexical Scoping)垃圾回收机制

正常情况:函数执行完,变量销毁

function outer() {
  let a = 10;
  console.log(a);
}
outer(); // 10
// 函数执行完毕,a 被垃圾回收器回收,内存释放。

特殊情况:内部函数引用了外部变量

function outer() {
  let a = 10;
  function inner() {
    console.log(a); // inner 引用了 outer 的变量 a
  }
  return inner; // 将 inner 返回出去
}
const myFunc = outer();
myFunc(); // 10

发生了什么?

  1. outer 执行完毕,按理说 a 应该被销毁。
  2. 但是,inner 函数被返回并赋值给了 myFunc
  3. inner 的定义中包含了对 a 的引用。
  4. JS 引擎发现:“嘿,inner 还在外面被人拿着呢,它还要用 a,所以我不能销毁 a!”
  5. 于是,a 所在的内存空间被保留下来,形成了闭包

2. 🛠️ 闭包的三大核心作用

闭包不仅仅是理论,它在实际开发中有三个非常强大的用途:

✅ 作用一:数据封装与私有变量(模拟私有属性)

在 ES6 Class 出现之前,JS 没有真正的“私有变量”。闭包是实现模块化和数据隐藏的核心手段。

场景:创建一个计数器,外部只能调用 increment,无法直接修改 count

function createCounter() {
  let count = 0; // 私有变量,外部无法直接访问
  return {
    increment: function () {
      count++;
      return count;
    },
    decrement: function () {
      count--;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.count); // undefined ✅ 无法直接访问

价值:保护数据不被意外篡改,提供清晰的 API 接口。

✅ 作用二:函数柯里化与参数复用

闭包可以“记住”之前传入的参数,从而实现函数的部分应用(Partial Application)。

场景:固定税率计算。

function makeTaxCalculator(taxRate) {
  // taxRate 被闭包“记住”了
  return function (price) {
    return price * taxRate;
  };
}
const calcVAT = makeTaxCalculator(0.13); // 增值税率 13%
const calcIncomeTax = makeTaxCalculator(0.2); // 所得税率 20%
console.log(calcVAT(100)); // 13
console.log(calcIncomeTax(100)); // 20

价值:减少重复传参,提高代码复用性。

✅ 作用三:异步回调中的状态保持

在异步编程(如定时器、AJAX、事件监听)中,闭包确保回调函数执行时,依然能访问到定义时的上下文变量。

场景:延迟打印日志。

function logAfterDelay(msg, delay) {
  setTimeout(function () {
    // 这里的 msg 和 delay 即使在 setTimeout 触发时(几秒后)依然可用
    console.log(`${msg} after ${delay}ms`);
  }, delay);
}
logAfterDelay("Hello", 1000);

3. 💻 代码实战:经典面试题解析

❌ 经典陷阱:循环中的闭包

这是面试中最常考的闭包问题。

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

预期输出:1, 2, 3, 4, 5
实际输出:6, 6, 6, 6, 6 😱

原因

  • var 没有块级作用域,所有定时器共享同一个全局变量 i
  • 当定时器执行时,循环已经结束,i变成了 6。

✅ 解决方案 1:使用 IIFE(立即执行函数)创建闭包

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j); // j 是每次循环独立的副本
    }, j * 1000);
  })(i);
}

原理:IIFE 为每次循环创建了一个新的作用域,将当前的 i 值作为参数 j 传入并保存。

✅ 解决方案 2:使用let(推荐)

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

原理let 具有块级作用域,JS 引擎会为每次循环迭代创建一个新的绑定。

4. ⚠️ 双刃剑:内存泄漏与性能

闭包虽然强大,但滥用会导致内存泄漏

📉 为什么会导致内存泄漏?

正常情况下,函数执行完后,局部变量会被回收。
但如果形成了闭包,且外部一直持有对该闭包函数的引用,那么闭包所引用的所有外部变量都不会被回收

危险示例

function hugeDataHandler() {
  const hugeArray = new Array(1000000).fill("data"); // 占用大量内存
  return function () {
    console.log(hugeArray.length); // 只要这个函数存在,hugeArray 就不会被回收
  };
}
const handler = hugeDataHandler();
// 如果你不再需要 handler,必须手动解除引用
handler = null; // ✅ 允许 GC 回收 hugeArray

🛡️ 最佳实践

  1. 及时解除引用:如果不再需要闭包函数,将其赋值为 null
  2. 避免在闭包中引用巨大的无用对象
  3. 谨慎使用全局闭包:尽量缩小闭包的作用范围。

💡 总结

特性说明
本质函数 + 引用的自由变量
核心能力延长变量生命周期,实现数据私有化
主要用途1. 封装私有变量
2. 柯里化/参数复用
3. 异步回调状态保持
副作用可能导致内存泄漏,增加内存占用
解决循环陷阱使用 let 或 IIFE

到此这篇关于JavaScript 闭包:函数背后的“背包”的文章就介绍到这了,更多相关js闭包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解JS鼠标事件中clientX/screenX/offsetX/pageX属性的区别

    详解JS鼠标事件中clientX/screenX/offsetX/pageX属性的区别

    在熟悉业务中播放器功能的时候,发现时间轴上绑定了点击和拖动事件,频繁遇到了类似pageX之类的事件属性,所以本文就来和大家详细聊聊clientX/screenX/offsetX/pageX等属性的区别吧
    2024-03-03
  • pnpm monorepo 联调方案问题解析

    pnpm monorepo 联调方案问题解析

    文章主要介绍了在pnpmmonorepo环境下进行多库联调的方案,包括使用`pnpmlink`命令来链接指定的文件夹或全局的`node_modules`,并在项目中通过`pnpmlink--global<pkg>`来引用这些库,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧
    2024-12-12
  • JS实现点击按钮获取页面高度的方法

    JS实现点击按钮获取页面高度的方法

    这篇文章主要介绍了JS实现点击按钮获取页面高度的方法,涉及JavaScript针对页面元素高度的各种常见运算,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • 在ES5与ES6环境下处理函数默认参数的实现方法

    在ES5与ES6环境下处理函数默认参数的实现方法

    本文给大家介绍在ES5与ES6环境下处理函数默认参数的实现方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,感兴趣的朋友跟随脚本之家小编一起学习吧
    2018-05-05
  • 兼容主流浏览器的iframe自适应高度js脚本

    兼容主流浏览器的iframe自适应高度js脚本

    兼容主流浏览器的 iframe 自适应高度,很实用的,具体的js脚本如下,感兴趣的朋友可不要错过
    2014-01-01
  • JavaScript中的私有成员

    JavaScript中的私有成员

    JavaScript中的私有成员...
    2006-09-09
  • 基于JS快速实现导航下拉菜单动画效果附源码下载

    基于JS快速实现导航下拉菜单动画效果附源码下载

    这是一个带变形动画特效的下拉导航菜单特效。该导航菜单在菜单项之间切换时,下拉菜单会快速的根据菜单内容的大小来动态变形,显示合适的下拉菜单大小,效果非常棒,对导航下拉菜单效果感兴趣的朋友一起通过本文学习吧
    2016-10-10
  • JavaScript实现左侧菜单效果

    JavaScript实现左侧菜单效果

    这篇文章主要为大家详细介绍了JavaScript实现左侧菜单效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-12-12
  • Array数组对象中的forEach、map、filter及reduce详析

    Array数组对象中的forEach、map、filter及reduce详析

    这篇文章主要给大家介绍了关于Array数组对象中forEach、map、filter及reduce的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用array数据具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-08-08
  • Vue编程式跳转的实例代码详解

    Vue编程式跳转的实例代码详解

    这篇文章主要介绍了Vue编程式跳转的实例代码,文中给大家介绍了vue 编程式js跳转路由,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-07-07

最新评论