javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > js闭包

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

作者: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 😱

原因

✅ 解决方案 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闭包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文