当前位置: 首页 > news >正文

用 “私房钱” 类比闭包:为啥它能访问外部变量?

深入浅出JavaScript闭包:你真的懂它吗?

哈喽,各位前端的小伙伴们!今天我们来聊一个让无数面试官“爱不释手”的话题——JavaScript闭包。别看它名字听起来有点“高冷”,其实理解起来就像剥洋葱,一层一层,越剥越有味儿!✨

💡 什么是闭包?

闭包,用大白话来说,就是一个“有特权的函数”。它能访问到它“出生”时所在的环境,即使那个环境已经“寿终正寝”了,它依然能“不忘初心”,访问到那些变量。是不是有点像你小时候藏在床底下的“私房钱”,即使你长大了搬家了,那笔钱还在那里等着你?💰

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

最常见的创建方式: 在一个函数内部创建另一个函数。

🔄 闭包的“超能力”:两大用途

闭包可不是个“花瓶”,它可是有真本事的!主要有两大“超能力”:

1. 访问“私密”变量 🔑

闭包的第一个用途,就是让外部世界能够访问到函数内部的“私密”变量。这就像给你的“私房钱”加了一把锁,只有特定的“钥匙”(闭包)才能打开。这样既能保护数据,又能灵活使用。

举个例子:

我们来思考一个简单的计数器。如果不用闭包,你可能会这么写:

// 初始化计数器
var a = 0;// 递增计数器的函数
function add() {a++;console.log(a);
}// 调用三次 add()
add(); // 1
add(); // 2  
add(); // 3

这个例子中,a 是一个全局变量,任何人都可以修改它,这就不太“安全”了。如果你的同事不小心把 a 改成了 100,那你的计数器就“乱套”了!

现在,我们用闭包来改造一下:

function add() {var a = 0; // a 现在是 add 函数的局部变量,外部无法直接访问return function adds() { // 这个 adds 函数就是闭包a++;console.log(a);};
}// 调用三次 add()
const xd = add(); // xd 现在是那个“有特权的函数” adds
xd(); // 1
xd(); // 2
xd(); // 3

看!现在 a 变量被“保护”起来了,外部只能通过 xd() 这个闭包来操作它,是不是安全多了?这就是闭包的魅力所在!

2. 变量“长生不老” ⏳

闭包的第二个用途,就是让那些本该“寿终正寝”的变量,能够“长生不老”,继续留在内存中。这就像你把一个重要的文件放在一个“保险箱”里,即使你离开了办公室,那个文件依然在保险箱里,不会被清理掉。

add() 函数执行完毕后,它的执行上下文会被销毁,但由于 adds() (闭包)引用了 add() 作用域中的 a 变量,所以 a 变量不会被垃圾回收机制回收,会一直保存在内存中,直到 adds() 不再被引用。

⚠️ 面试官最爱考的“坑”:循环中的闭包问题

说到闭包,就不得不提这个经典的面试题了!多少英雄好汉在这里“折戟沉沙”!

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

你猜猜这段代码会输出什么?是不是以为会依次输出 1, 2, 3, 4, 5

答案是: 会输出五次 6!😱

为什么呢? 因为 setTimeout 是一个异步函数,它不会立即执行。当 for 循环飞速地跑完之后,i 的值已经变成了 6。等到 setTimeout 里的 timer 函数真正执行的时候,它去访问 i,发现 i 已经是 6 了,所以就打印了五次 6

🔧 解决方案

别急,解决办法有三种,每种都体现了对闭包的深刻理解!

1. 使用闭包(立即执行函数)
for (var i = 1; i <= 5; i++) {(function(j) { // 立即执行函数,创建了一个新的作用域setTimeout(function timer() {console.log(j); // 这里的 j 捕获了每次循环的 i 值}, j * 1000);})(i); // 每次循环都把当前的 i 值传给 j
}

通过立即执行函数(IIFE),我们为每次循环创建了一个独立的作用域,将当前的 i 值作为参数 j 传递进去。这样,timer 函数引用的就是每次循环独立的 j,而不是循环结束后的全局 i

2. 使用 setTimeout 的第三个参数

这是一个比较“冷门”但很实用的方法!setTimeout 的第三个参数可以直接作为回调函数的参数传入。

for (var i = 1; i <= 5; i++) {setTimeout(function timer(i) { // 这里的 i 是 setTimeout 传入的参数console.log(i);},i * 1000,i // 将当前的 i 值作为第三个参数传入);
}

这种方法简洁明了,直接利用了 setTimeout 的特性,将每次循环的 i 值“固定”在了 timer 函数的参数中。

3. 使用 let 定义 i(最推荐的方式)

ES6 引入的 let 关键字完美解决了这个问题!这也是目前最推荐的方式。

for (let i = 1; i <= 5; i++) { // 使用 let 声明 isetTimeout(function timer() {console.log(i);}, i * 1000);
}

let 声明的变量具有块级作用域,每次循环都会创建一个新的 i。所以,setTimeout 里的 timer 函数会捕获到每次循环独立的 i 值,问题迎刃而解!是不是感觉 let 简直是“救星”?🌟

总结 📝

闭包是JavaScript中一个非常强大且重要的概念。它不仅能帮助我们实现数据封装和私有化,还能解决一些异步编程中的“老大难”问题。理解闭包,就像掌握了一项“魔法”,能让你的代码更加灵活、健壮。

希望通过这篇博客,你对闭包有了更深入的理解。下次面试再遇到它,你就可以自信满满地告诉面试官:我,懂闭包!😎

如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、转发哦!👍


http://www.lryc.cn/news/611753.html

相关文章:

  • Google Chrome <139.0.7236.0 UAF漏洞
  • RabbitMQ面试精讲 Day 12:镜像队列与Quorum队列对比
  • MATLAB下载教程MATLAB R2025a 保姆级安装步骤(附安装包)
  • 双馈和永磁风机构网型跟网型联合一次调频并入同步机电网,参与系统一次调频,虚拟惯量下垂,虚拟同步机VSG控制matlab/simulink
  • matlab——simulink学习(5向NXP库中添加新模块)
  • 计算机网络:如何判断B或者C类IP地址是否划分了子网
  • Linux之Shell脚本基本语法
  • 3步学会使用渲染101--3DMAX云渲染
  • 【计算机网络 | 第3篇】物理媒介
  • 【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现
  • 探索Linux MMC子系统的奥秘
  • TypeScript 元组类型精简知识点
  • 大数据存储域——HDFS存储系统
  • MCP协议与Spring AI框架实战
  • NY112NY117美光固态闪存NY119NY123
  • 新手向:Python实现简易计算器
  • 疯狂星期四文案网第30天运营日记
  • mysql索引的用法
  • Suno API V5模型 python源码 —— 使用灵感模式进行出创作
  • 国产3D大型装配设计新突破①:图纸打开设计双加速 | 中望3D 2026
  • rsync 的三种常见用法
  • 学习bug
  • jmm 指令重排 缓存可见性 Volatile 内存屏障
  • word2vector细致分解(CBOW, SKIP_GRAM, 层次soft Max, 负采样)
  • linux创建虚拟内存
  • Linux Vim 常用快捷键
  • Java学习第一百一十部分——CI/CD
  • 【完整源码+数据集+部署教程】爬行动物异常检测系统源码和数据集:改进yolo11-GhostDynamicConv
  • 一个php 连sqlserver 目标计算机积极拒绝,无法连接问题的解决
  • CVE-2021-30661、CVE-2021-30665、CVE-2021-30666