发布于

JavaScript 闭包机制深度解析:词法作用域、内存管理与实际应用

作者

JavaScript 闭包机制深度解析:词法作用域、内存管理与实际应用

闭包是 JavaScript 中最重要也是最容易被误解的概念之一。它不仅是语言的核心特性,更是许多高级编程模式的基础。本文将深入探讨闭包的工作原理和实际应用。

闭包的基本概念

什么是闭包

// 闭包的基本形式
function outerFunction(x) {
  // 外部函数的变量
  const outerVariable = x

  // 内部函数
  function innerFunction(y) {
    // 内部函数访问外部函数的变量
    console.log(`外部变量: ${outerVariable}, 内部参数: ${y}`)
    return outerVariable + y
  }

  // 返回内部函数
  return innerFunction
}

// 创建闭包
const closure = outerFunction(10)

// 调用闭包
console.log(closure(5)) // 输出: 外部变量: 10, 内部参数: 5, 返回: 15

// 即使外部函数已经执行完毕,内部函数仍然可以访问外部变量
console.log(closure(20)) // 输出: 外部变量: 10, 内部参数: 20, 返回: 30

词法作用域和作用域链

// 词法作用域演示
const globalVar = 'global'

function level1() {
  const level1Var = 'level1'

  function level2() {
    const level2Var = 'level2'

    function level3() {
      const level3Var = 'level3'

      // 内部函数可以访问所有外层作用域的变量
      console.log('全局变量:', globalVar)
      console.log('Level1 变量:', level1Var)
      console.log('Level2 变量:', level2Var)
      console.log('Level3 变量:', level3Var)

      // 返回一个函数,形成闭包
      return function () {
        return {
          global: globalVar,
          level1: level1Var,
          level2: level2Var,
          level3: level3Var,
        }
      }
    }

    return level3()
  }

  return level2()
}

// 创建闭包
const deepClosure = level1()

// 即使所有外层函数都已执行完毕,闭包仍然保持对变量的引用
console.log(deepClosure())
// 输出: { global: 'global', level1: 'level1', level2: 'level2', level3: 'level3' }

// 作用域链查找演示
function scopeChainDemo() {
  const a = 1

  function inner1() {
    const b = 2

    function inner2() {
      const c = 3
      const a = 'inner2' // 遮蔽外层的 a

      console.log('a:', a) // 'inner2' (最近的 a)
      console.log('b:', b) // 2 (从 inner1 作用域)
      console.log('c:', c) // 3 (当前作用域)

      // 访问全局作用域需要使用 window (浏览器) 或 global (Node.js)
      // console.log('outer a:', window.a) // 如果 a 是全局变量
    }

    return inner2
  }

  return inner1()
}

const scopeClosure = scopeChainDemo()
scopeClosure()

闭包的形成条件

不同情况下的闭包形成

// 1. 函数返回函数
function createCounter() {
  let count = 0

  return function () {
    return ++count
  }
}

const counter1 = createCounter()
const counter2 = createCounter()

console.log(counter1()) // 1
console.log(counter1()) // 2
console.log(counter2()) // 1 (独立的闭包)
console.log(counter1()) // 3

// 2. 函数作为参数传递
function processArray(arr, callback) {
  const multiplier = 2

  return arr.map(function (item, index) {
    // callback 形成闭包,可以访问外部变量
    return callback(item * multiplier, index)
  })
}

function createProcessor(prefix) {
  // 返回的函数形成闭包
  return function (value, index) {
    return `${prefix}[${index}]: ${value}`
  }
}

const processor = createProcessor('Item')
const result = processArray([1, 2, 3], processor)
console.log(result)
// 输出: ['Item[0]: 2', 'Item[1]: 4', 'Item[2]: 6']

// 3. 事件处理器中的闭包
function setupEventHandlers() {
  const buttons = document.querySelectorAll('.btn')
  const clickCounts = new Map()

  buttons.forEach((button, index) => {
    clickCounts.set(button, 0)

    // 事件处理器形成闭包
    button.addEventListener('click', function () {
      const currentCount = clickCounts.get(button) + 1
      clickCounts.set(button, currentCount)

      console.log(`按钮 ${index} 被点击了 ${currentCount}`)

      // 可以访问外部作用域的变量
      button.textContent = `点击次数: ${currentCount}`
    })
  })

  // 返回清理函数
  return function cleanup() {
    buttons.forEach((button) => {
      button.removeEventListener('click', arguments.callee)
    })
    clickCounts.clear()
  }
}

// 4. 定时器中的闭包
function createTimer(name, interval) {
  let seconds = 0

  const timerId = setInterval(function () {
    seconds++
    console.log(`${name}: ${seconds}`)

    // 闭包保持对外部变量的引用
    if (seconds >= 5) {
      clearInterval(timerId)
      console.log(`${name} 计时器结束`)
    }
  }, interval)

  // 返回控制函数
  return {
    stop: function () {
      clearInterval(timerId)
      console.log(`${name} 计时器被手动停止`)
    },
    getSeconds: function () {
      return seconds
    },
  }
}

const timer = createTimer('测试计时器', 1000)

// 5. 模块模式中的闭包
const modulePattern = (function () {
  // 私有变量
  let privateVar = 0
  const privateArray = []

  // 私有方法
  function privateMethod() {
    console.log('这是私有方法')
  }

  // 返回公共接口
  return {
    // 公共方法通过闭包访问私有变量
    increment: function () {
      privateVar++
      return privateVar
    },

    decrement: function () {
      privateVar--
      return privateVar
    },

    getValue: function () {
      return privateVar
    },

    addItem: function (item) {
      privateArray.push(item)
      privateMethod() // 调用私有方法
      return privateArray.length
    },

    getItems: function () {
      // 返回副本,保护私有数据
      return [...privateArray]
    },
  }
})()

console.log(modulePattern.increment()) // 1
console.log(modulePattern.getValue()) // 1
console.log(modulePattern.addItem('test')) // 1
console.log(modulePattern.getItems()) // ['test']

闭包的实际应用

1. 数据封装和私有变量

// 创建具有私有状态的对象
function createBankAccount(initialBalance) {
  let balance = initialBalance
  const transactionHistory = []

  // 私有方法
  function addTransaction(type, amount) {
    transactionHistory.push({
      type,
      amount,
      balance: balance,
      timestamp: new Date(),
    })
  }

  // 返回公共接口
  return {
    deposit: function (amount) {
      if (amount > 0) {
        balance += amount
        addTransaction('deposit', amount)
        return balance
      }
      throw new Error('存款金额必须大于0')
    },

    withdraw: function (amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount
        addTransaction('withdraw', amount)
        return balance
      }
      throw new Error('取款金额无效或余额不足')
    },

    getBalance: function () {
      return balance
    },

    getTransactionHistory: function () {
      // 返回副本,防止外部修改
      return transactionHistory.map((t) => ({ ...t }))
    },

    // 只读属性
    get accountInfo() {
      return {
        balance: balance,
        transactionCount: transactionHistory.length,
        lastTransaction: transactionHistory[transactionHistory.length - 1],
      }
    },
  }
}

const account = createBankAccount(1000)
console.log(account.deposit(500)) // 1500
console.log(account.withdraw(200)) // 1300
console.log(account.getBalance()) // 1300
console.log(account.accountInfo) // 账户信息

// 无法直接访问私有变量
console.log(account.balance) // undefined

2. 函数工厂和柯里化

// 函数工厂
function createValidator(rules) {
  return function (value) {
    const errors = []

    // 闭包访问 rules 参数
    for (const rule of rules) {
      if (!rule.test(value)) {
        errors.push(rule.message)
      }
    }

    return {
      isValid: errors.length === 0,
      errors: errors,
    }
  }
}

// 创建不同的验证器
const emailValidator = createValidator([
  {
    test: (value) => typeof value === 'string',
    message: '必须是字符串',
  },
  {
    test: (value) => value.includes('@'),
    message: '必须包含@符号',
  },
  {
    test: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: '邮箱格式不正确',
  },
])

const passwordValidator = createValidator([
  {
    test: (value) => typeof value === 'string',
    message: '必须是字符串',
  },
  {
    test: (value) => value.length >= 8,
    message: '密码长度至少8位',
  },
  {
    test: (value) => /[A-Z]/.test(value),
    message: '必须包含大写字母',
  },
  {
    test: (value) => /[0-9]/.test(value),
    message: '必须包含数字',
  },
])

console.log(emailValidator('test@example.com'))
console.log(passwordValidator('Password123'))

// 柯里化函数
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function (...nextArgs) {
        return curried.apply(this, args.concat(nextArgs))
      }
    }
  }
}

// 使用柯里化
const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

// 实用的柯里化应用
const multiply = curry((a, b) => a * b)
const double = multiply(2)
const triple = multiply(3)

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

const numbers = [1, 2, 3, 4, 5]
console.log(numbers.map(double)) // [2, 4, 6, 8, 10]
console.log(numbers.map(triple)) // [3, 6, 9, 12, 15]

3. 缓存和记忆化

// 记忆化函数
function memoize(fn) {
  const cache = new Map()

  return function (...args) {
    const key = JSON.stringify(args)

    if (cache.has(key)) {
      console.log('从缓存获取结果')
      return cache.get(key)
    }

    console.log('计算新结果')
    const result = fn.apply(this, args)
    cache.set(key, result)

    return result
  }
}

// 斐波那契数列(递归版本)
const fibonacci = memoize(function (n) {
  if (n <= 1) return n
  return fibonacci(n - 1) + fibonacci(n - 2)
})

console.log(fibonacci(10)) // 计算并缓存
console.log(fibonacci(10)) // 从缓存获取
console.log(fibonacci(15)) // 利用之前的缓存结果

// 带过期时间的缓存
function createCacheWithTTL(ttl = 60000) {
  // 默认1分钟
  const cache = new Map()

  return function memoizeWithTTL(fn) {
    return function (...args) {
      const key = JSON.stringify(args)
      const now = Date.now()

      if (cache.has(key)) {
        const { value, timestamp } = cache.get(key)

        if (now - timestamp < ttl) {
          console.log('从缓存获取结果')
          return value
        } else {
          console.log('缓存已过期,删除缓存')
          cache.delete(key)
        }
      }

      console.log('计算新结果')
      const result = fn.apply(this, args)
      cache.set(key, { value: result, timestamp: now })

      return result
    }
  }
}

const memoizeWithTTL = createCacheWithTTL(5000) // 5秒过期

const expensiveOperation = memoizeWithTTL(function (x, y) {
  // 模拟耗时操作
  let result = 0
  for (let i = 0; i < 1000000; i++) {
    result += x * y
  }
  return result
})

console.log(expensiveOperation(10, 20))
console.log(expensiveOperation(10, 20)) // 从缓存获取

4. 事件系统和观察者模式

// 使用闭包实现事件系统
function createEventEmitter() {
  const events = new Map()

  return {
    on: function (eventName, callback) {
      if (!events.has(eventName)) {
        events.set(eventName, [])
      }
      events.get(eventName).push(callback)

      // 返回取消订阅的函数
      return function unsubscribe() {
        const callbacks = events.get(eventName)
        if (callbacks) {
          const index = callbacks.indexOf(callback)
          if (index > -1) {
            callbacks.splice(index, 1)
          }
        }
      }
    },

    emit: function (eventName, ...args) {
      const callbacks = events.get(eventName)
      if (callbacks) {
        callbacks.forEach((callback) => {
          try {
            callback(...args)
          } catch (error) {
            console.error('事件处理器执行错误:', error)
          }
        })
      }
    },

    once: function (eventName, callback) {
      const unsubscribe = this.on(eventName, function (...args) {
        callback(...args)
        unsubscribe() // 执行一次后自动取消订阅
      })
      return unsubscribe
    },

    off: function (eventName) {
      events.delete(eventName)
    },

    getEventNames: function () {
      return Array.from(events.keys())
    },

    getListenerCount: function (eventName) {
      const callbacks = events.get(eventName)
      return callbacks ? callbacks.length : 0
    },
  }
}

// 使用事件系统
const emitter = createEventEmitter()

// 订阅事件
const unsubscribe1 = emitter.on('user:login', (user) => {
  console.log(`用户 ${user.name} 登录了`)
})

const unsubscribe2 = emitter.on('user:login', (user) => {
  console.log(`记录登录日志: ${user.name} at ${new Date()}`)
})

// 一次性事件
emitter.once('app:ready', () => {
  console.log('应用已准备就绪')
})

// 触发事件
emitter.emit('user:login', { name: 'John', id: 1 })
emitter.emit('app:ready')
emitter.emit('app:ready') // 不会再次触发

// 取消订阅
unsubscribe1()
emitter.emit('user:login', { name: 'Jane', id: 2 }) // 只有第二个监听器会执行

内存管理和性能考虑

闭包的内存影响

// 内存泄漏示例和解决方案
function createMemoryLeakExample() {
  const largeData = new Array(1000000).fill('large data')
  let timer

  return {
    // ❌ 可能导致内存泄漏
    startBadTimer: function () {
      timer = setInterval(function () {
        // 即使不使用 largeData,闭包仍然保持对它的引用
        console.log('定时器运行中...')
      }, 1000)
    },

    // ✅ 更好的做法
    startGoodTimer: function () {
      // 只保留需要的数据
      const dataSize = largeData.length

      timer = setInterval(function () {
        console.log(`定时器运行中... 数据大小: ${dataSize}`)
      }, 1000)

      // 释放大数据的引用
      // largeData = null // 注意:这样做在严格模式下会报错
    },

    stop: function () {
      if (timer) {
        clearInterval(timer)
        timer = null
      }
    },
  }
}

// 内存优化的闭包使用
function createOptimizedClosure() {
  let cache = new Map()
  let isDestroyed = false

  function checkDestroyed() {
    if (isDestroyed) {
      throw new Error('对象已被销毁')
    }
  }

  return {
    set: function (key, value) {
      checkDestroyed()
      cache.set(key, value)
    },

    get: function (key) {
      checkDestroyed()
      return cache.get(key)
    },

    // 提供清理方法
    destroy: function () {
      cache.clear()
      cache = null
      isDestroyed = true
    },

    // 获取内存使用情况
    getMemoryInfo: function () {
      checkDestroyed()
      return {
        size: cache.size,
        keys: Array.from(cache.keys()),
      }
    },
  }
}

// 弱引用闭包(使用 WeakMap)
function createWeakReferenceClosure() {
  const weakCache = new WeakMap()

  return {
    associate: function (obj, data) {
      weakCache.set(obj, data)
    },

    getAssociated: function (obj) {
      return weakCache.get(obj)
    },

    hasAssociation: function (obj) {
      return weakCache.has(obj)
    },
  }
}

// 使用示例
const weakClosure = createWeakReferenceClosure()
let obj = { id: 1 }

weakClosure.associate(obj, { metadata: 'some data' })
console.log(weakClosure.getAssociated(obj)) // { metadata: 'some data' }

// 当 obj 被垃圾回收时,WeakMap 中的关联也会自动清除
obj = null

性能优化建议

// 性能优化的闭包模式
function createPerformantClosure() {
  // 1. 避免在闭包中保存不必要的引用
  const config = {
    /* 配置对象 */
  }

  // 2. 使用对象池减少内存分配
  const objectPool = []

  function getPooledObject() {
    return objectPool.pop() || {}
  }

  function returnToPool(obj) {
    // 清理对象
    Object.keys(obj).forEach((key) => delete obj[key])
    objectPool.push(obj)
  }

  // 3. 延迟初始化
  let expensiveResource = null

  function getExpensiveResource() {
    if (!expensiveResource) {
      expensiveResource = createExpensiveResource()
    }
    return expensiveResource
  }

  return {
    processData: function (data) {
      const obj = getPooledObject()

      try {
        // 处理数据
        obj.result = data.map((item) => item * 2)
        return obj.result
      } finally {
        returnToPool(obj)
      }
    },

    useExpensiveResource: function () {
      return getExpensiveResource().doSomething()
    },
  }
}

function createExpensiveResource() {
  console.log('创建昂贵的资源')
  return {
    doSomething: () => 'expensive operation result',
  }
}

总结

JavaScript 闭包的核心要点和最佳实践:

🎯 核心概念

  1. 词法作用域:函数在定义时确定作用域
  2. 变量捕获:内部函数保持对外部变量的引用
  3. 生命周期延长:外部变量不会被垃圾回收
  4. 独立实例:每次调用外部函数都创建新的闭包

✅ 实际应用

  • 数据封装和私有变量
  • 模块模式和命名空间
  • 函数工厂和柯里化
  • 事件处理和回调函数
  • 缓存和记忆化

⚠️ 注意事项

  • 避免不必要的闭包创建
  • 及时清理引用防止内存泄漏
  • 注意循环中的闭包陷阱
  • 合理使用 WeakMap 和 WeakSet

🚀 性能优化

  • 避免在闭包中保存大对象
  • 提供清理方法
  • 使用对象池减少内存分配
  • 延迟初始化昂贵资源

掌握闭包,你就掌握了 JavaScript 的精髓!


闭包是 JavaScript 最强大的特性之一,理解它能让你写出更优雅、更强大的代码。