闭包:从“变量怎么还没死”到写出真正健壮的模块

重构一个用户权限系统时,遇到一个新人写的代码:

js 复制代码
function createUserSession(userData) {
  const user = userData
  const loginTime = Date.now()
  let token = null

  return {
    setToken(t) { token = t },
    getToken() { return token },
    getUser() { return user },
    getAge() { 
      return new Date().getFullYear() - user.birthYear 
    }
  }
}

const session = createUserSession({
  name: 'Alice',
  birthYear: 1995,
  role: 'admin'
})

session.setToken('abc123')
console.log(session.getToken()) // 'abc123'

他问我:"为什么 token 变量在函数执行完后还能被访问?它不应该被销毁吗?"

这就是 JavaScript 中最强大也最让人困惑的概念------闭包(Closure)


一、问题场景:如何安全地管理用户会话

我们有个后台管理系统,需要:

  1. 存储用户敏感信息(如 token)
  2. 提供操作接口(获取/更新 token)
  3. 禁止外部直接访问原始数据
  4. 支持多用户独立会话

如果不用闭包,可能会这样写:

js 复制代码
// ❌ 全局变量污染 + 数据不安全
let currentUser = null
let currentToken = null

function login(user, token) {
  currentUser = user
  currentToken = token
}

但这样任何人都可以直接修改 currentToken,太危险了。


二、解决方案:用闭包创建私有作用域

我们用闭包重构:

js 复制代码
function createAuthManager(initialUser) {
  // 🔍 私有变量:外部无法直接访问
  let user = initialUser
  let token = null
  const createdAt = Date.now()

  // 🔐 特权函数:可以访问私有变量的公共接口
  return {
    // 设置 token(带验证)
    setToken(t) {
      if (!t || typeof t !== 'string') {
        throw new Error('Invalid token')
      }
      token = t
      console.log('Token updated')
    },

    // 获取 token
    getToken() {
      return token ? token.slice(0, 5) + '...' : null // 🔐 只返回部分信息
    },

    // 获取用户信息
    getUser() {
      // 返回副本,防止外部修改原始数据
      return { ...user }
    },

    // 更新用户信息
    updateUser(updates) {
      user = { ...user, ...updates }
    },

    // 检查是否登录
    isLoggedIn() {
      return !!token && (Date.now() - createdAt) < 8 * 60 * 60 * 1000 // 8小时有效期
    },

    // 登出
    logout() {
      token = null
    }
  }
}

使用方式:

js 复制代码
const auth = createAuthManager({
  name: 'Alice',
  role: 'admin',
  birthYear: 1995
})

auth.setToken('jwt-token-12345')
console.log(auth.getToken())     // 'jwt-t...'
console.log(auth.getUser().name) // 'Alice'
auth.logout()
console.log(auth.isLoggedIn())   // false

三、原理剖析:从表面到内存管理的三层机制

1. 表面用法:什么是闭包?

当一个函数记住并访问它的词法作用域,即使这个函数在它的词法作用域外执行时,就产生了闭包。

我们来画一张 闭包内存引用图

graph TB A["[全局执行上下文]"] --> B["createAuthManager() 执行"] B --> C["创建局部变量:user, token, createdAt"] C --> D["返回对象(包含函数)"] D --> E["createAuthManager 执行结束"] E --> F["本该销毁局部变量"] F --> G["但!返回的函数内部引用了这些变量"] G --> H["JavaScript 引擎决定:保留这些变量在内存中"] H --> I["形成闭包:函数 + 词法环境的组合"] style A fill:#9f9,stroke:#333,stroke-width:2px style F fill:#ffd699,stroke:#333,stroke-width:2px style G fill:#f99,stroke:#333,stroke-width:2px style I fill:#d4edda,stroke:#333,stroke-width:2px

2. 底层机制:V8 引擎如何处理闭包?

当函数被定义时,JavaScript 会创建一个隐藏属性 [[Environment]],指向定义时的词法环境。

js 复制代码
function outer() {
  const secret = 'I am private'
  
  function inner() {
    console.log(secret) // inner.[[Environment]] → 指向 outer 的作用域
  }
  
  return inner
}

即使 outer 执行完毕,inner 仍然通过 [[Environment]] 引用着 secret 变量,因此垃圾回收器不会清理它。


3. 设计哲学:为什么需要闭包?

闭包实现了 数据封装状态持久化,解决了三个核心问题:

问题 闭包解决方案
全局变量污染 把数据放在函数作用域内
数据不安全 外部无法直接访问私有变量
状态丢失 函数执行完后状态仍保留

💡 类比:

闭包就像一个"带锁的保险箱"------

  • 箱子(函数作用域)里放着贵重物品(变量)
  • 只有特定钥匙(返回的函数)才能打开箱子
  • 即使你把钥匙交给别人,他也只能按规则取物,不能拆箱子

四、闭包的典型使用场景

1. 模块模式(Module Pattern)

js 复制代码
const Counter = (function() {
  let count = 0 // 私有状态

  return {
    increment() { return ++count },
    decrement() { return --count },
    value() { return count },
    reset() { count = 0 }
  }
})()

2. 函数工厂(Function Factory)

js 复制代码
function createMultiplier(factor) {
  return function(number) {
    return number * factor
  }
}

const double = createMultiplier(2)
const triple = createMultiplier(3)

console.log(double(5)) // 10
console.log(triple(5)) // 15

3. 事件处理与回调

js 复制代码
function setupButton(id, label) {
  const element = document.getElementById(id)
  
  element.addEventListener('click', function() {
    // 🔍 这里形成了闭包,可以访问 label 变量
    console.log(`Button ${label} clicked`)
  })
}

4. 防抖与节流

js 复制代码
function debounce(func, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => func.apply(this, args), delay)
  }
}

const saveDraft = debounce(() => {
  console.log('Saving draft...')
}, 1000)

五、特权函数的使用场景

"特权函数"是指那些既能被外部调用,又能访问私有变量的函数。它们是闭包最强大的应用。

场景1:带验证的数据访问器

js 复制代码
function createBankAccount(initialBalance) {
  let balance = initialBalance

  return {
    // 特权函数:带验证的取款
    withdraw(amount) {
      if (amount > balance) {
        throw new Error('余额不足')
      }
      balance -= amount
      return balance
    },

    // 特权函数:带日志的存款
    deposit(amount) {
      console.log(`存入 ${amount}`)
      balance += amount
      return balance
    },

    // 只读访问
    getBalance() {
      return balance
    }
  }
}

场景2:配置管理器

js 复制代码
function createConfigManager(defaults) {
  let config = { ...defaults }

  return {
    // 合并新配置
    update(newConfig) {
      config = { ...config, ...newConfig }
    },

    // 获取配置(防止外部修改)
    get(key) {
      return key ? config[key] : { ...config }
    },

    // 重置
    reset() {
      config = { ...defaults }
    }
  }
}

场景3:单例模式

js 复制代码
const Logger = (function() {
  let instance = null
  let logs = []

  function log(message) {
    logs.push({
      timestamp: Date.now(),
      message
    })
    console.log(message)
  }

  return {
    getInstance() {
      if (!instance) {
        instance = { log, getLogs: () => [...logs] }
      }
      return instance
    }
  }
})()

六、实战避坑指南

❌ 错误用法:在循环中创建闭包不注意绑定

js 复制代码
// 常见错误
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i) // 输出 3, 3, 3
  }, 100)
}

✅ 正确做法:使用 let 或 IIFE

js 复制代码
// 方案1:用 let(块级作用域)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i) // 0, 1, 2
  }, 100)
}

// 方案2:用 IIFE
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(() => {
      console.log(index)
    }, 100)
  })(i)
}

❌ 错误用法:闭包导致内存泄漏

js 复制代码
function handleHugeData() {
  const hugeData = new Array(1000000).fill('data')

  // 即使不需要 hugeData 了,只要这个事件监听器存在,它就不会被回收
  window.addEventListener('resize', () => {
    console.log('Resize')
  })
}

✅ 正确做法:及时解绑或置空

js 复制代码
function handleHugeData() {
  const hugeData = new Array(1000000).fill('data')
  let handler

  handler = () => {
    console.log('Resize')
    // 使用完后解绑
    window.removeEventListener('resize', handler)
    // 帮助垃圾回收
    handler = null
    hugeData = null
  }

  window.addEventListener('resize', handler)
}

七、举一反三:三个变体场景实现思路

  1. 需要多个实例共享私有数据

    在外层再包一层闭包,让多个实例共享同一个私有作用域(如连接池管理)。

  2. 实现私有类字段(ES6 Class)

    结合 WeakMap 实现真正的私有属性,避免闭包长期持有引用。

  3. 缓存计算结果(记忆化)

    用闭包保存计算结果,避免重复运算。

js 复制代码
function memoize(fn) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) {
      return cache.get(key)
    }
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}

小结

闭包不是"意外的功能",而是 JavaScript 中实现 封装、状态管理和模块化的核心机制。

记住这个口诀:

函数内变量,函数外还能用,
作用域链锁,内存中永留存。

当你需要:

  • 创建私有变量
  • 保持状态
  • 生成定制函数
  • 实现模块化

闭包就是你的第一选择

而特权函数,就是你暴露给世界的"安全接口"------它既能保护内部数据,又能提供灵活的操作能力。

相关推荐
paopaokaka_luck27 分钟前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失9491 小时前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_1 小时前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路4 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔4 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang4 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔4 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任4 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴4 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js