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

闭包(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 被保护在闭包内,只能通过暴露的方法修改,避免全局污染。

 

// 计数器(你的小金库) function 创建小金库() { let 余额 = 0; // 藏在闭包里的钱,外部无法直接修改 return { 存钱: (金额) => { 余额 += 金额; }, 查余额: () => { return 余额; }}; } const 我的小金库 = 创建小金库(); 我的小金库.存钱(100); console.log(我的小金库.查余额()); // 100 ✅

  • 闭包的作用:余额 藏在书包里,只通过特定方法操作,防止别人偷钱(保护数据)。


2. 回调函数(保留上下文)
 

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秒后输出 "响应数据"

 

// 记住你的偏好(比如网页主题) 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秒没人按按钮**,电梯才会关门。

  • 本质:**只响应最后一次连续操作**,避免频繁触发。

 

// 代码示例(搜索框输入) // 闭包的作用:用闭包保存 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));

节流 ——「水龙头滴水」的哲学

想象你拧开水龙头:

  • 规则:无论你拧得多快,水龙头 **每秒最多滴一滴水**,多余的触发被忽略。

  • 本质:**固定时间间隔内只触发一次**,稀释触发频率。

 

// 代码示例(窗口滚动事件) // 闭包的作用:用闭包保存 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 暴露在全局,防止污染其他代码(类似电梯的倒计时器只能由电梯自己控制)。

如果没有闭包会怎样?
 

// 没有闭包时,只能将计时器存在全局 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)); // 问题:当两个输入框同时触发时,它们会互相清除对方的计时器! // 结果:只有一个请求会被发送,另一个被取消。

 

// 没有闭包时,只能将上一次执行时间存在全局 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. 内存泄漏
 

function leakMemory() { const bigData = new Array(1000000).fill("⚠️"); // 大数据 return function() { console.log("闭包引用了 bigData,导致它无法被回收!"); }; } const leakedFunc = leakMemory(); // bigData 一直被闭包引用,无法释放

  • 解决方法:在不需要时手动解除引用(如 leakedFunc = null)。

 

// 内存泄漏: 如果书包里装了大石头(大数据),又不扔,书包会越来越重(内存占用)。 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
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

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

    余额充值