终于理解闭包是什么了(含防抖、节流)

终于理解闭包是什么了(含防抖、节流)

闭包(Closure)是编程中一个非常重要的概念,尤其在 JavaScript 中广泛使用。它的核心是函数和其周围状态(词法环境)的组合,可以简单理解为“函数记住了自己被创建时的环境”。


闭包的核心定义

  • 闭包是一个函数,它可以访问并记住自己定义时的作用域(即使该作用域已经执行完毕)。
  • 本质:函数内部嵌套的函数,能够“捕获”外部函数的变量,并长期保留这些变量的引用。
用「背包」来类比

假设你是一个学生,每天背着书包去上学。闭包就像你的书包:

  • 书包里有什么? 装着你需要的课本、文具(相当于函数内部需要的变量)。
  • 书包的规则: 你可以在教室里(函数内部)往书包里放东西,即使放学回家了(函数执行完毕) ,书包里的东西依然存在,第二天还能继续用。
  • 关键点: 书包里的东西(变量)只属于你,别人拿不到(私有性)。

闭包的本质: 函数放学回家了(执行完毕),但它依然背着自己的书包(保存了函数创建时的作用域变量),随时可以打开书包用里面的东西。


举个直观的例子 🌰
function outer() {
  const name = "Alice"; // outer 函数的局部变量

  function inner() {
    console.log(name); // inner 函数访问 outer 的变量
  }

  return inner; // 返回 inner 函数
}

const myFunc = outer(); // outer 执行完毕,按理说 name 应该被销毁
myFunc(); // 输出 "Alice" ✅——闭包让 inner 记住了 name 的值!
发生了什么?
  1. outer() 执行后,其作用域按理应该被销毁。
  2. inner 函数被返回,且它引用了 outer 中的变量 name
  3. 闭包保留了 name** 的引用**,因此 myFunc() 仍然能访问到 name

闭包的三大特性

  1. 访问外部作用域:内部函数可以访问外部函数的变量。
    1. 灵活控制: 可以用闭包动态生成不同功能的小工具(如多个独立计数器)。
  2. 变量长期存活:即使外部函数已执行完毕,其变量也不会被垃圾回收。
    1. 记住过去: 函数执行后,依然能记住之前的状态(比如游戏存档)。
  3. 变量私有化:闭包中的变量对外部不可见,实现数据封装。
    1. 保护隐私: 像你的日记本,只有你自己能修改(封装私有变量)。

闭包的经典应用场景

1. 模块化开发(封装私有变量)
function createCounter() {
  let count = 0; // 私有变量,外部无法直接访问

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

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
  • 优势count 被保护在闭包内,只能通过暴露的方法修改,避免全局污染。
javascript 体验AI代码助手 代码解读复制代码 // 计数器(你的小金库)
function 创建小金库() {
    let 余额 = 0; 
    
    // 藏在闭包里的钱,外部无法直接修改
    return {
        存钱: (金额) => { 余额 += 金额; },
        查余额: () => { return 余额; }};
    }
    const 我的小金库 = 创建小金库();
我的小金库.存钱(100);
console.log(我的小金库.查余额()); // 100 ✅
  • 闭包的作用:余额 藏在书包里,只通过特定方法操作,防止别人偷钱(保护数据)。

2. 回调函数(保留上下文)
javascript 体验AI代码助手 代码解读复制代码function fetchData(url) {
  let data = null;

  // 模拟异步请求
  setTimeout(() => {
    data = "响应数据";
  }, 1000);

  return { 
    then: (callback) => { 
      setTimeout(() => {
        callback(data); // 回调函数通过闭包访问 data
      }, 1000);
    }
  };
}

fetchData("https://api.example.com")
  .then((data) => console.log(data)); // 1秒后输出 "响应数据"
javascript 体验AI代码助手 代码解读复制代码// 记住你的偏好(比如网页主题)
function 主题切换器(初始主题) {
  let 当前主题 = 初始主题; // 藏在闭包里的主题

  return {
    切换主题: () => {
      当前主题 = 当前主题 === 'light' ? 'dark' : 'light';
      document.body.style.background = 当前主题 === 'dark' ? '#333' : '#fff';
    }
  };
}

const 切换按钮 = 主题切换器('light');
document.getElementById('themeBtn').addEventListener('click', 切换按钮.切换主题);

//闭包的作用: 点击按钮时,函数依然记得 当前主题 的值(即使函数已经执行完),实现状态持久化。

3. 防抖(Debounce)和节流(Throttle)
防抖 ——「电梯关门」的哲学

想象你在电梯里:

  • 规则:电梯门打开后,如果 10秒内 有人按开门按钮,电梯会重新计时10秒;直到 连续10秒没人按按钮,电梯才会关门。
  • 本质只响应最后一次连续操作,避免频繁触发。
javascript 体验AI代码助手 代码解读复制代码 // 代码示例(搜索框输入)
// 闭包的作用: 用闭包保存 timer 变量,让多次触发的回调函数共享同一个计时器(类似电梯记住倒计时状态)。

function debounce(func, delay) {
  let timer; // 藏在闭包里的计时器(电梯的倒计时)

  return function(...args) {
    clearTimeout(timer); // 每次输入都重置计时(类似按开门按钮)
    timer = setTimeout(() => {
      func.apply(this, args); // 延迟结束后执行搜索(电梯关门)
    }, delay);
  };
}

const 输入框 = document.getElementById('search');
输入框.addEventListener('input', debounce(() => {
  console.log('发送搜索请求...'); // 只在用户停止输入后触发
}, 500));
节流 ——「水龙头滴水」的哲学

想象你拧开水龙头:

  • 规则:无论你拧得多快,水龙头 每秒最多滴一滴水,多余的触发被忽略。
  • 本质固定时间间隔内只触发一次,稀释触发频率。
javascript 体验AI代码助手 代码解读复制代码// 代码示例(窗口滚动事件)
// 闭包的作用:用闭包保存 lastTime 变量,记录上一次执行时间,让多次触发的回调函数共享这个时间戳。

function throttle(func, interval) {
  let lastTime = 0; // 藏在闭包里的上一次执行时间(记录上一次滴水时间)

  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) { // 距离上次执行超过间隔时间
      func.apply(this, args); // 执行函数(滴一滴水)
      lastTime = now; // 更新记录时间
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  console.log('处理滚动逻辑...'); // 每 200ms 最多触发一次
}, 200));
防抖 vs 节流的区别
场景防抖(Debounce)节流(Throttle)
核心思想事件停止后才触发固定间隔触发一次
类比电梯关门水龙头滴水
典型应用搜索框输入、窗口 resize 结束滚动事件、鼠标移动、射击游戏连发
代码差异每次触发重置计时器根据时间间隔判断是否执行
闭包的关键作用
  • 状态持久化:防抖和节流需要记住 timerlastTime,这些变量必须在多次函数调用间共享
  • 变量私有化:避免将 timerlastTime 暴露在全局,防止污染其他代码(类似电梯的倒计时器只能由电梯自己控制)。
如果没有闭包会怎样?
javascript 体验AI代码助手 代码解读复制代码// 没有闭包时,只能将计时器存在全局
let globalTimer; // 💥 全局变量,多个防抖函数会互相覆盖

function debounce(func, delay) {
  return function() {
    clearTimeout(globalTimer); // 所有防抖共享同一个计时器
    globalTimer = setTimeout(func, delay);
  };
}

// 页面中有两个输入框:
const input1 = document.getElementById('input1');
const input2 = document.getElementById('input2');

input1.addEventListener('input', debounce(() => {
  console.log('搜索框1的请求');
}, 500));

input2.addEventListener('input', debounce(() => {
  console.log('搜索框2的请求');
}, 500));

// 问题:当两个输入框同时触发时,它们会互相清除对方的计时器!
// 结果:只有一个请求会被发送,另一个被取消。
javascript 体验AI代码助手 代码解读复制代码// 没有闭包时,只能将上一次执行时间存在全局
let globalLastTime = 0; // 💥 所有节流共享同一个时间戳

function throttle(func, interval) {
  return function() {
    const now = Date.now();
    if (now - globalLastTime >= interval) {
      func();
      globalLastTime = now;
    }
  };
}

// 页面中有两个需要节流的操作:
window.addEventListener('scroll', throttle(() => {
  console.log('处理滚动逻辑');
}, 200));

document.addEventListener('mousemove', throttle(() => {
  console.log('处理鼠标移动');
}, 200));

// 问题:滚动和鼠标移动共享同一个时间戳!
// 结果:滚动触发后,鼠标移动的逻辑会被强制延迟,反之亦然。

闭包的潜在问题

1. 内存泄漏
javascript 体验AI代码助手 代码解读复制代码function leakMemory() {
  const bigData = new Array(1000000).fill("⚠️"); // 大数据

  return function() {
    console.log("闭包引用了 bigData,导致它无法被回收!");
  };
}

const leakedFunc = leakMemory(); // bigData 一直被闭包引用,无法释放
  • 解决方法:在不需要时手动解除引用(如 leakedFunc = null)。
javascript 体验AI代码助手 代码解读复制代码// 内存泄漏: 如果书包里装了大石头(大数据),又不扔,书包会越来越重(内存占用)。
function 装石头() {
  const 大石头 = new Array(1000000).fill("重"); 
  return () => { console.log(大石头[0]); };
}
const 沉重的书包 = 装石头(); // 大石头一直存在内存中!

// 解决: 不用时把书包置空(沉重的书包 = null)。

2. 意外的闭包(常见于循环)
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出 3 次 3 ❗(var 没有块级作用域)
  }, 100);
}
  • 解决:用 let 替代 var(利用块级作用域),或使用闭包隔离:

    • for (var i = 0; i < 3; i++) {
        (function(j) { // 立即执行函数创建新作用域
          setTimeout(() => {
            console.log(j); // 输出 0, 1, 2 ✅
          }, 100);
        })(i);
      }
      

闭包与其他语言

  • JavaScript:闭包是核心特性,天然支持。
  • Python/Go/Rust:支持闭包,但可能需要显式声明捕获变量。
  • Java/C++ :通过匿名内部类或 Lambda 表达式模拟闭包,限制较多。

总结

  • 闭包的核心:函数 + 定义时的词法环境。
  • 优点:封装数据、模块化开发、保留上下文。
  • 注意事项:避免内存泄漏、正确处理循环中的闭包。
  • 哲学:闭包是“时间胶囊”,让函数穿越时空,记住过去的状态
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT枫斗者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值