🌟 面试必考、开发必踩:大白话彻底搞懂 JS 闭包(附 Vue3 避坑指南)

0 阅读5分钟

🌟 面试必考、开发必踩:大白话彻底搞懂 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 项目中优化高频触发事件(如 scrollinput)的必备技巧。核心必须要用闭包来保存 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)来练练手吗?