3、闭包到底闭了谁:项目中闭包的5个应用与陷阱

102 阅读3分钟

💡 本篇目标:从编译原理层面彻底搞懂闭包的本质;结合实际开发中的 5 类典型应用场景,剖析闭包的价值与风险;并指出常见误用与内存泄漏问题。


🎯 一、闭包是什么?为什么这么多人搞不懂?

📖 定义:

闭包是函数能够“记住并访问”它定义时所在作用域的变量,即使函数是在其作用域外被调用。

🚨 误区澄清:

  • ❌ 闭包不是“函数中嵌套函数”
  • ✅ 闭包是函数 + 其定义时作用域的组合

🧠 二、本质:函数内部持有外部变量的引用

function outer() {
  const message = 'Hello'
  return function inner() {
    console.log(message)
  }
}

const fn = outer()
fn() // 输出 'Hello'

✅ 内部结构抽象(简化):

fn → {
  code: function inner() {...},
  [[Environment]]: {
    message: 'Hello'
  }
}
  • inner 函数执行时,依赖 [[Environment]] 访问其外部变量
  • 这就是闭包机制

🔍 三、闭包的 5 大实战应用

1️⃣ 封装私有变量

function createCounter() {
  let count = 0
  return {
    inc: () => ++count,
    get: () => count
  }
}
const c = createCounter()
c.inc() // 1
c.inc() // 2

🧠 应用:模块化、数据保护、限制访问权限


2️⃣ 模拟 once 执行器(只执行一次)

function once(fn) {
  let called = false
  return function (...args) {
    if (!called) {
      called = true
      return fn.apply(this, args)
    }
  }
}
const fire = once(() => console.log('🔥 Fire!'))
fire() // 打印
fire() // 忽略

🧠 应用:登录按钮防多次点击、API 初始化控制


3️⃣ 延迟执行 & 缓存数据

function memoizedAdd() {
  let cache = {}
  return (n) => {
    if (n in cache) return cache[n]
    const result = n + 10
    cache[n] = result
    return result
  }
}
const add = memoizedAdd()
add(5) // 15
add(5) // 缓存返回

🧠 应用:缓存中间结果,提高性能


4️⃣ 定时器或异步中的“变量保留”

function delayLog() {
  for (var i = 0; i < 3; i++) {
    setTimeout(function () {
      console.log(i)
    }, 1000)
  }
}
delayLog() // 输出 3 3 3 ❌

✅ 修正方法一:立即执行函数 IIFE

for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(() => console.log(i), 1000)
  })(i)
}

✅ 方法二:使用 let(块级作用域)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000)
}

5️⃣ 事件监听器封装

function bindEvent(elem, type, countLimit) {
  let count = 0
  function handler() {
    if (++count <= countLimit) {
      console.log(`第${count}次点击`)
    } else {
      elem.removeEventListener(type, handler)
    }
  }
  elem.addEventListener(type, handler)
}

🧠 应用:限制触发次数、防止频繁绑定解绑逻辑


🧨 四、闭包的常见陷阱

❌ 1. 循环中创建闭包绑定 var

var fns = []
for (var i = 0; i < 3; i++) {
  fns.push(() => console.log(i))
}
fns[0]() // 3 ❌

原因:所有箭头函数共享同一个 i 引用,循环结束后 i = 3

✅ 解法:let / IIFE


❌ 2. 闭包导致内存泄漏(被意外引用)

function bindTooltip(elem) {
  const tooltip = document.createElement('div')
  tooltip.innerText = '信息提示'
  elem.addEventListener('mouseenter', function () {
    tooltip.style.display = 'block'
  })
  // ❗未移除 listener,即便 DOM 删除,tooltip 被引用着
}

🧽 最佳实践:

  • 移除事件监听器
  • 避免无谓引用外部变量(如 DOM)

❌ 3. 滥用闭包导致性能下降

function createButtonHandlers() {
  const handlers = []
  for (let i = 0; i < 1000; i++) {
    handlers.push(() => console.log(i))
  }
  return handlers
}
  • 过度使用闭包,导致 1000 个函数实例驻留内存
  • 可以考虑统一 handler + 传参方式优化

📦 五、工程实践建议

使用闭包的目的推荐做法
数据封装使用返回函数的形式
事件绑定保证解除绑定或使用 WeakMap
模块缓存保证内存控制,设置失效机制
大量闭包创建尽量复用 handler,减少不必要引用

🧭 总结回顾

  • 闭包是函数 + 环境变量记录的组合
  • 是 JS 模块封装、延迟执行、权限控制的核心机制
  • 常见陷阱包括:循环 var、未释放引用、内存泄漏
  • 项目中使用闭包要明确边界与释放策略

📘 下一篇《第4篇:let/const 暗藏的块级作用域行为》,我们将深入探讨为什么 let 是闭包场景的“神兵利器”,并解析 TDZ、块作用域、循环变量绑定等问题。