🌟 面试必考、开发必踩:大白话彻底搞懂 JS 闭包(附 Vue3 避坑指南)
引言
各位掘友们好!在前端的进阶之路上,闭包(Closure) 绝对是一座绕不开的大山。无论是日常开发中封装组件、做性能优化,还是在面试中与面试官过招,它都是出场率极高的 C 位概念。
很多人觉得闭包很“玄学”,看了无数遍定义还是云里雾里。今天,我们就抛开晦涩的官方文档,用最直白的大白话,把闭包扒个底朝天!
一、 到底什么是闭包?(别背定义,看比喻)
MDN 官方是这么说的:
闭包是由函数以及声明该函数的词法环境组合而成的。
这句话对于新手来说等于没说。我们换个接地气的理解方式:
你可以把 JS 中的函数想象成一个人,而**“作用域”**就是他出生的老家。通常情况下,一个人离开老家去外地打工(函数在其他地方被调用),他是带不走老家的东西的。
但是!闭包就是这个人的“随身背包” 。
当一个嵌套函数离开它出生的外层函数时,它会把外层函数里的变量打包塞进自己的背包里。无论这个嵌套函数走到哪里被执行,它都能随时从背包里掏出这些变量来用。
一句话总结:函数 + 它的随身背包(自带的外部变量) = 闭包。
二、 闭包在实战中的三大“杀手锏”
理解了概念,我们来看看闭包在日常搬砖中到底能干嘛。
1. 模拟私有变量(数据封装)
JS 早期没有真正的私有属性。我们不希望内部的状态被外部随意篡改,闭包完美解决了这个问题。
JavaScript
function createBank() {
let money = 1000; // money 是私有变量,外部绝对拿不到
return {
save: function(amount) {
money += amount;
console.log(`存入 ${amount},当前余额: ${money}`);
},
spend: function(amount) {
if (money >= amount) {
money -= amount;
console.log(`消费 ${amount},当前余额: ${money}`);
}
}
};
}
const myAccount = createBank();
myAccount.save(500);
myAccount.money = 999999; // 毫无作用!你无法直接修改内部变量
2. 防抖 (Debounce) 与节流 (Throttle)
这是面试手写题的常客,也是 Vue 项目中优化高频触发事件(如 scroll、input)的必备技巧。核心必须要用闭包来保存 timer 状态!
JavaScript
function debounce(fn, delay) {
let timer = null; // 这个 timer 会被保存在闭包里
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
3. 预置参数(函数柯里化)
当你需要固定函数的某些参数,生成一个新函数时。
JavaScript
function makeUrl(domain) {
return function(path) {
return `https://${domain}/${path}`;
}
}
const juejinUrl = makeUrl('juejin.cn');
console.log(juejinUrl('post/123')); // https://juejin.cn/post/123
三、 Vue3 专属避坑:闭包带来的“暗面”
由于 Vue3 的 ref 永远返回对象的引用(.value),我们几乎碰不到 React 那种“拿不到最新值”的陈旧闭包问题。但在 Vue3 中,闭包的杀伤力主要集中在以下两个坑点:
坑点 1:onMounted 里的定时器/原生事件导致“内存泄漏”
在单页应用(SPA)中,组件会频繁挂载和销毁。如果你的闭包绑定在了全局对象(如 window)或定时器上,即使组件被销毁了,闭包依然存活,内存直接泄漏!
Code snippet
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const count = ref(0)
let timer = null;
onMounted(() => {
// 这里的箭头函数是一个闭包,它悄悄把当前组件的 count 装进了背包
timer = setInterval(() => {
count.value++
console.log('正在执行...', count.value)
}, 1000)
})
// ❌ 致命错误:如果不写下面这段代码,
// 就算这个 Vue 组件被 v-if 销毁了,后台依然会疯狂打印,内存永远不会释放!
onUnmounted(() => {
clearInterval(timer) // 正确做法:人走茶凉,必须清理!
})
</script>
坑点 2:防抖函数写错位置,导致“多组件实例状态污染”
如果你在 Vue 组件的 <script> 外层(或者单独的 JS 文件里)定义了一个带闭包的防抖函数,由于这个闭包在模块加载时只初始化了一次,所有使用这个组件的实例,将会共享同一个 timer!
Code snippet
<script>
// ❌ 错误示范:写在 setup 外面。
// 闭包里的 timer 被所有该组件的实例共享!
// A 组件疯狂点击,会导致 B 组件的请求也被取消。
const submitDebounce = debounce(() => { /* 提交逻辑 */ }, 500)
</script>
<script setup>
// ✅ 正确做法:应该写在 setup 内部(或者直接使用 VueUse 的 useDebounceFn)
// 这样每个组件实例挂载时,都会生成一个属于自己独立的闭包背包!
const submitDebounce = debounce(() => { /* 提交逻辑 */ }, 500)
</script>
四、 总结
闭包不是什么魔法,它只是 JS 词法作用域的一个自然产物。
- 好处: 保护变量安全,维持状态不丢失。
- 坏处: 容易造成内存泄漏(千万记住在 Vue3 的
onUnmounted里擦屁股)。
只要你搞懂了那个“随身背包”的比喻,以后无论代码怎么绕,你都能一眼看穿它的本质!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、评论交流!你的支持是我持续输出的最大动力~ 🚀
标签:#JavaScript #Vue3 #前端面试 #闭包 #性能优化
Vue3 配合闭包最优雅的玩法,其实是去封装自定义的 Hooks(Composables / 组合式函数) ,比如大名鼎鼎的 VueUse 库底层就大量使用了这种思想。
你目前对 Vue3 的组合式函数(Composables)封装熟悉吗?需要我带你手写一个企业级的 useDebounce (防抖 Hook)来练练手吗?