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

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

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

💡 什么是闭包?

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

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

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

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

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

1. 访问"私密"变量 🔑

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

举个例子:

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

javascript 复制代码
// 初始化计数器
var a = 0;

// 递增计数器的函数
function add() {
    a++;
    console.log(a);
}

// 调用三次 add()
add(); // 1
add(); // 2  
add(); // 3

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

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

javascript 复制代码
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() 不再被引用。

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

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

javascript 复制代码
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. 使用闭包(立即执行函数)
javascript 复制代码
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 的第三个参数可以直接作为回调函数的参数传入。

javascript 复制代码
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 关键字完美解决了这个问题!这也是目前最推荐的方式。

javascript 复制代码
for (let i = 1; i <= 5; i++) { // 使用 let 声明 i
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

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

总结 📝

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

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

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


相关推荐
拉不动的猪3 分钟前
# 关于初学者对于JS异步编程十大误区
前端·javascript·面试
玖釉-8 分钟前
解决PowerShell执行策略导致的npm脚本无法运行问题
前端·npm·node.js
Larcher42 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐1 小时前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭1 小时前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信2 小时前
我们需要了解的Web Workers
前端
brzhang2 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js