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

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

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

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


相关推荐
寅时码5 分钟前
我开源了一款 Canvas “瑞士军刀”,十几种“特效与工具”开箱即用
前端·开源·canvas
CF14年老兵7 分钟前
🚀 React 面试 20 题精选:基础 + 实战 + 代码解析
前端·react.js·redux
CF14年老兵8 分钟前
2025 年每个开发人员都应该知道的 6 个 VS Code AI 工具
前端·后端·trae
十五_在努力11 分钟前
参透 JavaScript —— 彻底理解 new 操作符及手写实现
前端·javascript
典学长编程24 分钟前
前端开发(HTML,CSS,VUE,JS)从入门到精通!第四天(DOM编程和AJAX异步交互)
javascript·css·ajax·html·dom编程·异步交互
拾光拾趣录27 分钟前
🔥99%人答不全的安全链!第5问必翻车?💥
前端·面试
IH_LZH31 分钟前
kotlin小记(1)
android·java·前端·kotlin
lwlcode39 分钟前
前端大数据渲染性能优化 - 分时函数的封装
前端·javascript
Java技术小馆40 分钟前
MCP是怎么和大模型交互
前端·面试·架构
玲小珑44 分钟前
Next.js 教程系列(二十二)代码分割与打包优化
前端·next.js