当前位置: 首页 > 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.xdnf.cn/news/1253449.html

相关文章:

  • 【AI智能编程】Trae-IDE工具学习
  • vector使用模拟实现
  • 排序算法(二)
  • Qt-桌面宠物
  • win10/11网络防火墙阻止网络连接?【图文详解】防火墙阻止连接网络的解决方法
  • Unity 调节 Rigidbody2D 响应速度的解决方案【资料】
  • GPT-OSS-20B vs Qwen3-14B 全面对比测试
  • AI领域的三箭齐发之夜 - genie3,gpt-oss, Opus 4.1
  • K8S的POD数量限制
  • harbor仓库搭建(配置https)
  • 数据结构(4)
  • 时间轮算法
  • 【算法训练营Day21】回溯算法part3
  • vue3 el-dialog自定义实现拖拽、限制视口范围增加了拖拽位置持久化的功能
  • DNS 服务器
  • 【golang】基于redis zset实现并行流量控制(计数锁)
  • 部署Web UI自动化测试平台:SeleniumFlaskTester
  • Maven入门到精通
  • Rust进阶-part5-trait
  • 机器学习——朴素贝叶斯
  • 19day-人工智能-机器学习-分类算法-决策树
  • NCD57080CDR2G 安森美onsemi 通用驱动器, SOIC, 8针, 20V电源, 8 A输出NCD57080CDR2电流隔离式栅极驱动器
  • 大模型后训练——Online-RL基础
  • 【嵌入式电机控制#26】BLDC:三相模拟采集
  • 江协科技STM32 15-1 FLASH闪存
  • LinkedList 深度解析:核心原理与实践
  • Docker 常用命令介绍
  • Linux 中 Git 操作大全
  • Web 端 AI 图像生成技术的应用与创新:虚拟背景与创意图像合成
  • 初识神经网络01——认识PyTorch