{T}

闭包

闭包(Closure)是 JavaScript 中最重要也是最容易被误解的概念之一。理解闭包对于掌握 JavaScript 的核心特性至关重要。

概述

定义

闭包是指有权访问另一个函数作用域中变量的函数。更准确地说,闭包是由函数及其词法环境组合而成的实体。

核心特征

  1. 函数嵌套:闭包通常是在一个函数内部创建另一个函数
  2. 变量引用:内部函数引用外部函数的变量
  3. 环境保持:即使外部函数执行完毕,内部函数仍然可以访问外部函数的变量

为什么需要闭包?

闭包解决了以下问题:

javascript
// 问题:函数执行完后,局部变量会被销毁
function counter() {
  let count = 0
  console.log(count)
}
counter() // 0
// count 变量已被销毁,无法再次访问

// 解决方案:使用闭包保持变量
function createCounter() {
  let count = 0
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  }
}

const myCounter = createCounter()
myCounter.increment() // 1
myCounter.increment() // 2
// count 变量仍然存在,被闭包保持

理论基础

词法作用域

闭包的形成依赖于 JavaScript 的词法作用域(Lexical Scoping)机制。

javascript
// 词法作用域:函数的作用域在定义时确定,而非调用时
function outer() {
  const x = 10
  
  // inner 在定义时就确定了它可以访问 outer 的作用域
  function inner() {
    console.log(x) // 可以访问 x,因为词法作用域规则
  }
  
  return inner
}

const fn = outer()
fn() // 10

作用域链示意图

code
┌─────────────────────────────────────┐
│  Global Scope                       │
│  ┌───────────────────────────────┐  │
│  │  outer() Scope                │  │
│  │  - x = 10                     │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │  inner() Scope          │  │  │
│  │  │  - 访问 x (通过作用域链)│  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

闭包的本质 ⚠️

形成条件

闭包的形成需要满足三个条件:

  1. 函数嵌套:存在内部函数
  2. 变量引用:内部函数引用外部函数的变量
  3. 生命周期延续:内部函数被传递到外部或被保存

简单示例

javascript
function outer() {
  const outerVar = 'outer'
  
  function inner() {
    console.log(outerVar)  // 访问外部函数的变量
  }
  
  return inner
}

const closure = outer()
closure()  // 'outer'

执行过程分析

javascript
function createCounter() {
  let count = 0
  
  return {
    increment: function () {
      count++
      console.log(count)
    },
    decrement: function () {
      count--
      console.log(count)
    },
    getCount: function () {
      return count
    }
  }
}

const counter = createCounter()
counter.increment()  // 1
counter.increment()  // 2
counter.decrement()  // 1
console.log(counter.getCount())  // 1

执行流程:

code
1. createCounter() 被调用
   ├─ 创建执行上下文
   ├─ 创建变量 count = 0
   └─ 返回对象(包含三个闭包函数)

2. createCounter() 执行完毕
   ├─ 通常情况:执行上下文被销毁,count 被回收
   └─ 实际情况:返回的函数引用 count,count 被保留在闭包中

3. counter.increment() 被调用
   └─ 通过作用域链访问闭包中的 count 变量

闭包的特性

1. 访问外部函数的变量

javascript
function foo() {
  const a = 1
  
  function bar() {
    console.log(a)  // 访问外部函数的变量 a
  }
  
  return bar
}

const baz = foo()
baz()  // 1

2. 保持对外部变量的引用

javascript
function createClosure() {
  let value = 0
  
  return function () {
    value++
    console.log(value)
  }
}

const closure = createClosure()
closure()  // 1
closure()  // 2
closure()  // 3

// value 变量不会被垃圾回收,因为闭包仍然引用它

3. 独立的作用域(闭包之间的隔离)

javascript
function createClosures() {
  const closures = []
  
  for (var i = 0; i < 3; i++) {
    closures.push(function () {
      console.log(i)
    })
  }
  
  return closures
}

const closures = createClosures()
closures[0]()  // 3
closures[1]()  // 3
closures[2]()  // 3

// 所有闭包共享同一个 i 变量
// 循环结束后 i = 3

解决方案:

javascript
// 方案1:使用 let(推荐)
function createClosuresWithLet() {
  const closures = []
  
  for (let i = 0; i < 3; i++) {  // let 为每次迭代创建新的绑定
    closures.push(function () {
      console.log(i)
    })
  }
  
  return closures
}

// 解决方案1:使用 let 创建块级作用域
function createClosuresLet() {
  const closures = []
  for (let i = 0; i < 5; i++) {
    closures.push(function () {
      console.log(i)  // 每次迭代独立的 i
    })
  }
  return closures
}

// 解决方案2:使用 IIFE 立即执行函数
function createClosuresIIFE() {
  const closures = []
  for (var i = 0; i < 5; i++) {
    (function (index) {
      closures.push(function () {
        console.log(index)  // 捕获每次迭代的参数
      })
    })(i)
  }
  
  return closures
}

闭包的应用场景 ⚠️

1. 数据私有化

闭包是实现数据私有化的经典方式,类似于其他语言中的私有属性。

javascript
// 基本示例
function createUser(name) {
  let _name = name  // 私有变量
  let _age = 0      // 私有变量
  
  return {
    getName: function () {
      return _name
    },
    setName: function (newName) {
      _name = newName
    },
    getAge: function () {
      return _age
    },
    setAge: function (newAge) {
      _age = newAge
    }
  }
}

// 进阶:银行账户私有状态
function createBankAccount(initialBalance) {
  let balance = initialBalance  // 私有变量,外部无法直接访问
  
  return {
    deposit: function (amount) {
      balance += amount
      console.log(`存款 ${amount},当前余额: ${balance}`)
    },
    withdraw: function (amount) {
      if (amount > balance) {
        console.log('余额不足')
        return
      }
      balance -= amount
      console.log(`取款 ${amount},当前余额: ${balance}`)
    },
    getBalance: function () {
      return balance
    }
  }
}

const account = createBankAccount(1000)
account.deposit(500)   // 1500
account.withdraw(200)  // 1300
console.log(account.getBalance())  // 1300
console.log(account.balance)       // undefined

2. 函数工厂

创建配置化函数,提高代码复用性。

javascript
// 基本示例
function createMultiplier(multiplier) {
  return function (number) {
    return number * multiplier
  }
}

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

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

// 进阶:创建验证器工厂(组合多个规则)
function createValidator(rules) {
  return function (value) {
    for (const rule of rules) {
      const error = rule(value)
      if (error) return error
    }
    return null
  }
}

const validateEmail = createValidator([
  value => value ? null : 'Email is required',
  value => !value.includes('@') ? 'Invalid email format' : null,
])

console.log(validateEmail(''))          // 'Email is required'
console.log(validateEmail('invalid'))   // 'Invalid email format'
console.log(validateEmail('test@example.com'))  // null

3. 回调函数与事件处理

保持事件处理器的状态。

javascript
// 点击计数器
function setupCounter(element) {
  let count = 0
  
  element.addEventListener('click', function () {
    count++
    console.log('Clicked', count, 'times')
    this.textContent = `Clicked ${count} times`
  })
}

const button = document.querySelector('button')
setupCounter(button)

// 进阶:动画队列(通过闭包维护执行顺序)
function createAnimationQueue() {
  const queue = []
  let isPlaying = false
  
  function playNext() {
    if (queue.length === 0 || isPlaying) return
    isPlaying = true
    const current = queue.shift()
    current(() => {
      isPlaying = false
      playNext()
    })
  }
  
  return {
    add(fn) {
      queue.push(fn)
      playNext()
    }
  }
}

const animator = createAnimationQueue()
animator.add(() => fadeIn(element1))
animator.add(() => fadeOut(element2))

4. 防抖和节流

性能优化的经典应用。

javascript
// 防抖(Debounce):延迟执行,期间再次触发则重新计时
function debounce(func, delay) {
  let timer = null
  
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}

// 使用防抖:搜索输入
const searchInput = document.getElementById('search')
const handleSearch = debounce((query) => {
  console.log('搜索:', query)
}, 300)
searchInput.addEventListener('input', (e) => handleSearch(e.target.value))

// 节流(Throttle):固定时间间隔内只执行一次
function throttle(func, limit) {
  let inThrottle = false
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => { inThrottle = false }, limit)
    }
  }
}

// 使用节流:滚动监听
const handleScroll = throttle((scrollPos) => {
  updateUI(scrollPos)
}, 100)

window.addEventListener('scroll', () => {
  handleScroll(window.scrollY)
})

5. 柯里化函数

将多参数函数转换为单参数函数序列。

javascript
// 手动柯里化
function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}

// 柯里化日志记录器
function log(level, timestamp, message) {
  console.log(`[${timestamp}] [${level}] ${message}`)
}

const curriedLog = curry(log)
const errorLog = curriedLog('ERROR')
const errorLogWithTime = errorLog(new Date().toISOString())

errorLogWithTime('Something went wrong')

6. 模块模式

实现模块化代码组织。

javascript
// 基本模块模式
const module = (function () {
  let privateVar = 'private'
  
  function privateMethod() {
    console.log('private method')
  }
  
  return {
    publicVar: 'public',
    publicMethod: function () {
      console.log(privateVar)  // 闭包访问私有变量
      privateMethod()
    }
  }
})()

module.publicMethod()  // 'private' + 'private method'
// module.privateVar  // undefined(私有)

// 揭露模块模式(Revealing Module Pattern)
const calculator = (function () {
  let result = 0
  
  function add(x) { result += x; return result }
  function subtract(x) { result -= x; return result }
  function reset() { result = 0; return result }
  
  // 揭露公共接口
  return {
    add: add,
    subtract: subtract,
    reset: reset
  }
})()

7. 状态管理

实现简单的状态管理。

javascript
// 创建状态容器
function createStore(initialState) {
  let state = initialState
  const listeners = []
  
  return {
    getState: function () {
      return state
    },
    setState: function (newState) {
      state = typeof newState === 'function' 
        ? newState(state) 
        : newState
      // 通知所有订阅者
      listeners.forEach(listener => listener(state))
    },
    subscribe: function (listener) {
      listeners.push(listener)
      // 返回取消订阅函数
      return function () {
        const index = listeners.indexOf(listener)
        if (index !== -1) listeners.splice(index, 1)
      }
    }
  }
}

// 使用
const store = createStore({ count: 0, user: null })
const unsubscribe = store.subscribe((state) => {
  console.log('State changed:', state)
})

store.setState({ count: 1 })  // State changed: { count: 1, user: null }
store.setState({ user: { name: 'John' } })  // State changed: { count: 1, user: { name: 'John' } }

unsubscribe()  // 取消订阅

8. 缓存与记忆化

javascript
// 记忆化函数
function memoize(func) {
  const cache = new Map()
  
  return function (...args) {
    const key = JSON.stringify(args)
    
    if (cache.has(key)) {
      console.log('From cache')
      return cache.get(key)
    }
    
    const result = func.apply(this, args)
    cache.set(key, result)
    return result
  }
}

// 进阶:带过期时间的记忆化
function memoizeWithTTL(func, ttl = 60000) {
  const cache = new Map()
  
  return function (...args) {
    const key = JSON.stringify(args)
    
    // 检查是否过期
    if (cache.has(key)) {
      const entry = cache.get(key)
      if (Date.now() - entry.timestamp < ttl) {
        return entry.value
      }
      cache.delete(key)  // 已过期,删除
    }
    
    const result = func.apply(this, args)
    cache.set(key, { value: result, timestamp: Date.now() })
    return result
  }
}

闭包的问题与陷阱 ⚠️

1. 内存占用与内存泄漏

问题分析

闭包会保持对外部变量的引用,导致这些变量无法被垃圾回收。

javascript
// 问题示例:不必要的内存占用
function createClosure() {
  const largeData = new Array(1000000).fill('data')
  
  return function () {
    console.log(largeData.length)  // 只使用了 length,但整个数组都被保留
  }
}

const closure = createClosure()
// largeData 不会被垃圾回收,占用大量内存

解决方案

javascript
// 方案1:只保留需要的值
function createClosureOptimized() {
  const largeData = new Array(1000000).fill('data')
  const length = largeData.length  // 只保存需要的值
  
  return function () {
    console.log(length)
  }
  // largeData 可以被回收
}

// 方案2:手动释放引用
function createClosureManualRelease() {
  let largeData = new Array(1000000).fill('data')
  
  const handler = function () {
    console.log(largeData.length)
  }
  
  // 提供清理方法,用完手动释放
  handler.release = function () {
    largeData = null  // 释放对大数据的引用
  }
  
  return handler
}
const optimized = createClosureManualRelease()
optimized()
optimized.release()  // 手动释放

// 方案3:使用 WeakMap 关联数据(自动释放)
const privateData = new WeakMap()

function createUserWithPrivate(name) {
  const user = { name }
  privateData.set(user, { secretData: 'hidden' })
  
  return user
}

// 当 user 对象被销毁时,WeakMap 中的数据也会自动释放

2. 循环变量问题

经典陷阱

javascript
// ❌ 问题代码:所有函数共享同一个 i
for (var i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i)
  }, i * 1000)
}
// 输出:6 6 6 6 6(循环结束后 i = 6)

问题原因

code
循环结束时的状态:
┌─────────────────────────────┐
│  i = 6                      │
│  ┌─────────────────────┐    │
│  │ setTimeout #1       │ ───┼─→ 引用 i = 6
│  ├─────────────────────┤    │
│  │ setTimeout #2       │ ───┼─→ 引用 i = 6
│  ├─────────────────────┤    │
│  │ setTimeout #3       │ ───┼─→ 引用 i = 6
│  ├─────────────────────┤    │
│  │ setTimeout #4       │ ───┼─→ 引用 i = 6
│  ├─────────────────────┤    │
│  │ setTimeout #5       │ ───┼─→ 引用 i = 6
│  └─────────────────────┘    │
└─────────────────────────────┘

解决方案对比

javascript
// ✅ 方案1:使用 let(推荐)
for (let i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i)
  }, i * 1000)
}
// 输出:1 2 3 4 5
// 原理:let 为每次迭代创建新的词法环境

// ✅ 方案2:IIFE(立即执行函数表达式)
for (var i = 1; i <= 5; i++) {
  ((j) => {
    setTimeout(function () {
      console.log(j)
    }, j * 1000)
  })(i)
}
// 输出:1 2 3 4 5
// 原理:IIFE 将 i 作为参数传入,每次迭代创建独立作用域

// ✅ 方案3:setTimeout 传参
for (var i = 1; i <= 5; i++) {
  setTimeout(function (j) {
    console.log(j)
  }, i * 1000, i)
}
// 输出:1 2 3 4 5

3. this 指向问题

问题场景

javascript
const object = {
  name: 'object',
  getName: function () {
    return function () {
      console.log(this.name)  // undefined
    }
  }
}

object.getName()()
// this 指向全局对象(严格模式下为 undefined)

// 为什么?
// 内部函数作为独立函数调用,this 默认绑定到全局对象

解决方案

javascript
// ✅ 方案1:保存 this 引用
const object1 = {
  name: 'object',
  getName: function () {
    const self = this  // 或 const that = this
    return function () {
      console.log(self.name)  // 'object'
    }
  }
}

// ✅ 方案2:使用箭头函数(推荐)
const object2 = {
  name: 'object',
  getName: function () {
    // 箭头函数不绑定 this,继承外层
    return () => {
      console.log(this.name)  // 'object'
    }
  }
}
object2.getName()()  // 'object'

// ✅ 方案3:使用 bind 绑定
const object4 = {
  name: 'object',
  getName: function () {
    return (function () {
      console.log(this.name)
    }).bind(this)  // 显式绑定外层 this
  }
}

const fn = object4.getName()
fn.call(object4)  // 'object'

4. 性能陷阱

不必要的闭包创建

javascript
// ❌ 问题代码:循环中重复创建闭包
function setupHandlers(elements) {
  for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', function () {
      console.log('clicked')
    })
  }
}

// ✅ 优化:共享同一个函数
function handler() {
  console.log('clicked')
}

function setupHandlersOptimized(elements) {
  for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', handler)
  }
}

过度使用闭包

javascript
// ❌ 不必要的闭包
function process(data) {
  return function () {
    console.log(data)
  }
}

// ✅ 直接使用
function process(data) {
  console.log(data)
}

闭包的底层原理 ⚠️

执行上下文与变量对象

JavaScript 代码执行时,会创建执行上下文(Execution Context),每个执行上下文包含:

  • 变量对象(VO):存储变量和函数声明
  • 作用域链:指向父级作用域
  • this 值
javascript
// 示例分析
var globalVar = 'global'

function outer() {
  var outerVar = 'outer'
  
  function inner() {
    var innerVar = 'inner'
    console.log(innerVar, outerVar, globalVar)
  }
  
  return inner
}

const closure = outer()
closure()

执行过程可视化:

code
1. 全局执行上下文
┌─────────────────────────────┐
│  Variable Object            │
│  - globalVar: 'global'      │
│  - outer: function          │
│  - closure: undefined       │
└─────────────────────────────┘

2. outer() 执行上下文
┌─────────────────────────────┐
│  Activation Object          │
│  - outerVar: 'outer'        │
│  - inner: function (引用AO)  │
└─────────────────────────────┘

3. inner() 函数被定义时
- inner 的 [[Scope]] 指向 outer 的 Activation Object
- 闭包形成:inner 可访问 outer 的变量 outerVar

4. outer() 执行完毕
- 正常情况:outer.AO 被销毁
- 实际情况:inner 函数引用 outer.AO,保留在闭包中

词法环境(ES6+)

ES6 引入了词法环境(Lexical Environment)的概念,更清晰地解释闭包。

javascript
function createCounter() {
  let count = 0
  
  return {
    increment: () => ++count,
    decrement: () => --count
  }
}

const counter = createCounter()

词法环境结构:

code
createCounter 的词法环境:
┌────────────────────────────────┐
│  Environment Record            │
│  - count: 0                    │
│                                │
│  Outer Reference: global       │
└────────────────────────────────┘
         ↑
         │ 引用
         │
┌────────────────────────────────┐
│  increment/decrement 函数      │
│  [[Environment]]: 指向上面的    │
│  词法环境                       │
└────────────────────────────────┘

闭包与作用域链

javascript
var a = 1

function outer() {
  var a = 2
  
  function middle() {
    var a = 3
    
    function inner() {
      console.log(a)  // 3,从内到外查找
    }
    
    return inner
  }
  
  return middle
}

const closure = outer()()
closure()  // 3

// inner 函数的作用域链:
// [inner.AO, middle.AO, outer.AO, global.VO]
// 查找变量 a 时,从左到右查找,找到第一个就停止

作用域链示意图:

code
┌──────────────────────────────────────┐
│  inner.AO                            │
│  - (没有 a)                          │
└──────────────┬───────────────────────┘
               │
               ↓
┌──────────────────────────────────────┐
│  middle.AO                           │
│  - a: 3 ← 找到了!                   │
└──────────────┬───────────────────────┘
               │
               ↓
┌──────────────────────────────────────┐
│  outer.AO                            │
│  - a: 2                              │
└──────────────┬───────────────────────┘
               │
               ↓
┌──────────────────────────────────────┐
│  global.VO                           │
│  - a: 1                              │
└──────────────────────────────────────┘

性能优化 ⚠️

1. 及时释放闭包

javascript
// ❌ 问题:闭包一直占用内存
function createCache() {
  let cache = {}
  
  return {
    get: function (key) {
      return cache[key]
    },
    set: function (key, value) {
      cache[key] = value
    },
    // 提供销毁方法,主动释放闭包持有的数据
    destroy: function () {
      for (const key in cache) {
        delete cache[key]
      }
      cache = null
      console.log('缓存已销毁')
    }
  }

}

const cache = createCacheWithCleanup()
cache.set('key', 'value')
console.log(cache.get('key'))  // 'value'
cache.destroy()  // 手动释放内存

2. 避免不必要的闭包

javascript
// ❌ 不必要的闭包
function process(data) {
  return function () {
    console.log(data)
  }
}

// ✅ 直接使用
function process(data) {
  console.log(data)
}

// ✅ 共享 handler,避免每个元素创建新闭包
function createHandler(label) {
  return function (event) {
    console.log(label, event.target)
  }
}
const sharedHandler = createHandler('clicked')

function setupHandlers(elements) {
  for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', sharedHandler)
  }
}

3. 优化闭包内存占用

javascript
// ❌ 闭包持有大对象
function createProcessor() {
  const largeData = new Array(1000000).fill('data')
  
  return {
    process: function () {
      return largeData.length  // 只使用 length
    }
  }
}

// ✅ 只保留需要的数据(提取 length,不持有整个大数组)
function createProcessorOptimized() {
  const largeData = new Array(1000000).fill('data')
  const length = largeData.length  // 只保存需要的值
  
  return {
    process: function () {
      return length
    }
  }
}
// largeData 可被 GC 回收,闭包只持有 length

// ✅ 使用 WeakMap 存储大数据(自动释放)
const privateData = new WeakMap()

function createProcessorWeak() {
  const instance = {}
  privateData.set(instance, { largeData: new Array(1000000).fill('data') })
  
  return {
    process: function () {
      const data = privateData.get(instance)
      return data.largeData.length
    }
  }
}
// 当 instance 被销毁时,WeakMap 中的数据也会自动释放

4. 批量操作优化

javascript
// ❌ 多次创建闭包
function addClickHandlers(elements) {
  elements.forEach(element => {
    element.addEventListener('click', function () {
      console.log(element.id)
    })
  })
}

// ✅ 共享事件委托
function addClickHandlersOptimized(container) {
  container.addEventListener('click', function (e) {
    if (e.target.matches('.clickable')) {
      console.log(e.target.id)
    }
  })
}

5. 使用工具检测内存泄漏

javascript
// Chrome DevTools 内存分析示例
function createMemoryLeak() {
  const leaks = []
  
  return function () {
    const largeArray = new Array(10000).fill('leak')
    leaks.push(largeArray)  // 持续积累
    console.log(leaks.length)
  }
}

const leak = createMemoryLeak()
setInterval(leak, 1000)

// 使用 Chrome DevTools:
// 1. 打开 Memory 面板
// 2. 选择 "Heap snapshot"
// 3. 多次触发闭包
// 4. 对比快照,查找内存增长

最佳实践 ⚠️

1. 何时使用闭包

推荐使用:

  • 数据私有化
  • 函数工厂
  • 回调和事件处理
  • 模块化
  • 状态管理
  • 性能优化(防抖、节流、缓存)

避免使用:

  • 简单的数据传递
  • 不需要保持状态的场景
  • 性能敏感的代码路径
javascript
// ✅ 合理使用:数据私有化
function createUser(name) {
  let _name = name
  return {
    getName: () => _name,
    setName: (newName) => _name = newName
  }
}

// ❌ 过度使用:简单数据传递
function process(data) {
  return function () {
    return data  // 没有必要
  }
}

// ✅ 简化为
function process(data) {
  return data
}

2. 命名约定

javascript
// 私有变量使用下划线前缀
function createUser(name) {
  let _name = name
  let _age = 0
  
  return {
    getName: () => _name,
    getAge: () => _age
  }
}

// 使用语义化的闭包名称
const createCounter = () => { /* ... */ }
const createValidator = () => { /* ... */ }
const createCache = () => { /* ... */ }

3. 清理资源

javascript
// 提供清理方法
function createResource() {
  let resource = acquireResource()
  
  return {
    use: () => {
      return resource.doSomething()
    },
    dispose: () => {
      resource.release()
      resource = null
    }
  }
}

// 使用 try-finally 确保清理
const res = createResource()
try {
  res.use()
} finally {
  res.dispose()
}

4. 文档化闭包

javascript
/**
 * 创建一个计数器
 * @returns {Object} 计数器对象
 * @property {Function} increment - 增加计数
 * @property {Function} decrement - 减少计数
 * @property {Function} getCount - 获取当前计数
 * @property {Function} reset - 重置计数器
 */
function createCounter(initialValue = 0) {
  let count = initialValue
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
    reset: () => (count = initialValue)
  }
}

5. 测试闭包

javascript
// 测试闭包的行为
function testCounter() {
  const counter = createCounter(0)
  
  console.assert(counter.getCount() === 0, 'Initial count should be 0')
  console.assert(counter.increment() === 1, 'Increment should return 1')
  console.assert(counter.increment() === 2, 'Increment should return 2')
  console.assert(counter.decrement() === 1, 'Decrement should return 1')
  console.assert(counter.reset() === 0, 'Reset should return 0')
}

// 测试闭包的隔离性
function testClosureIsolation() {
  const counter1 = createCounter(0)
  const counter2 = createCounter(10)
  
  counter1.increment()
  counter2.decrement()
  
  console.assert(counter1.getCount() === 1, 'Counter1 should be 1')
  console.assert(counter2.getCount() === 9, 'Counter2 should be 9')
}

常见问题 ❓

Q1: 闭包和作用域有什么区别?

答: 作用域是变量的可访问范围,闭包是函数及其词法环境的组合。

javascript
// 作用域:变量的可访问性
function outer() {
  const x = 1  // x 的作用域是 outer 函数内部
  
  function inner() {
    console.log(x)  // inner 可以访问 x(作用域链)
  }
}

// 闭包:函数 + 词法环境
function createClosure() {
  const x = 1
  
  return function () {
    console.log(x)  // 这个函数 + x 变量 = 闭包
  }
}

const closure = createClosure()
// createClosure 已执行完毕,但 closure 仍可以访问 x
// 这就是闭包的作用

Q2: 所有函数都是闭包吗?

答: 从技术角度来说,所有函数在创建时都会捕获其词法环境,但通常我们说的闭包是指实际使用了外部变量的函数

javascript
function outer() {
  const x = 1
  
  // ✅ 是闭包:引用外部变量
  function inner() {
    console.log(x)
  }
  
  // ❓ 技术上是闭包,但通常不这样认为
  function notUsingX() {
    console.log('hello')
  }
  
  return { inner, notUsingX }
}

Q3: 闭包会导致内存泄漏吗?

答: 闭包本身不会导致内存泄漏,但不当使用会造成内存占用过高。

javascript
// ❌ 内存泄漏:忘记清理
function createLeak() {
  const data = []
  
  return {
    add: (item) => data.push(item),
    // 没有清理方法,data 会一直增长
  }
}

// ✅ 正确使用:提供清理方法
function createNoLeak() {
  let data = []
  
  return {
    add: (item) => data.push(item),
    clear: () => (data = [])
  }
}

Q4: 如何判断一个函数是否是闭包?

答: 判断标准:

  1. 函数是否嵌套在另一个函数内部
  2. 内部函数是否引用了外部函数的变量
  3. 内部函数是否被传递到外部或被返回
javascript
function outer() {
  const a = 1
  
  // ✅ 是闭包:满足所有条件
  function inner() {
    console.log(a)  // 引用外部变量
  }
  return inner  // 被返回
  
  // ❌ 不是闭包:没有引用外部变量
  function notClosure() {
    console.log('hello')
  }
  return notClosure
}

Q5: 闭包和立即执行函数(IIFE)有什么关系?

答: IIFE 常用于创建闭包来隔离作用域。

javascript
// IIFE 创建闭包
const counter = (function () {
  let count = 0
  
  return {
    increment: () => ++count,
    decrement: () => --count
  }
})()

// 等价于
function createCounter() {
  let count = 0
  
  return {
    increment: () => ++count,
    decrement: () => --count
  }
}

const counter = createCounter()

Q6: 闭包在 ES6 中还有必要吗?

答: ES6 的块级作用域(let/const)解决了一些问题,但闭包仍然非常重要。

javascript
// ES5: 使用闭包解决循环问题
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100)
  })(i)
}

// ES6: 使用 let 更简洁
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}

// 但闭包的其他应用仍然不可替代:
// - 数据私有化
// - 函数工厂
// - 模块模式
// - 状态管理

Q7: 如何在闭包中正确使用 this?

答: 使用箭头函数或保存 this 引用。

javascript
const obj = {
  name: 'object',
  
  // ❌ this 丢失
  method1: function () {
    return function () {
      console.log(this.name)  // undefined
    }
  },
  
  // ✅ 使用箭头函数
  method2: function () {
    return () => {
      console.log(this.name)  // 'object'
    }
  },
  
  // ✅ 保存 this 引用
  method3: function () {
    const self = this
    return function () {
      console.log(self.name)  // 'object'
    }
  },
  
  // ✅ 使用 bind 绑定
  method4: function () {
    return function () {
      console.log(this.name)
    }.bind(this)
  }
}

Q8: 闭包的性能影响有多大?

答: 现代 JavaScript 引擎对闭包进行了优化,性能影响通常很小,但需要注意:

javascript
// ❌ 性能问题:循环中创建大量闭包
function slow() {
  const handlers = []
  for (let i = 0; i < 10000; i++) {
    handlers.push(() => console.log(i))
  }
  return handlers
}

// ✅ 优化:按需创建
function fast() {
  const handlers = {
    get: (i) => () => console.log(i)
  }
  return handlers
}

总结

核心要点

  1. 定义:闭包是指有权访问另一个函数作用域中变量的函数,由函数及其词法环境组成
  2. 形成条件:函数嵌套 + 变量引用 + 生命周期延续
  3. 核心特性
    • 访问外部函数的变量
    • 保持对外部变量的引用
    • 实现数据私有化
    • 创建独立作用域

最佳实践

  • ✅ 使用闭包实现数据私有化和模块化
  • ✅ 使用 let/const 避免循环变量陷阱
  • ✅ 及时释放不再使用的闭包(置为 null
  • ✅ 使用 WeakMap 存储与大对象关联的私有数据

注意事项

  • ⚠️ 避免循环变量陷阱,使用 let 或 IIFE
  • ⚠️ 注意 this 指向问题,使用箭头函数或 bind
  • ⚠️ 避免过度使用闭包,在合适的场景使用

学习路径

code
初级:理解闭包的基本概念
  ↓
中级:掌握闭包的应用场景
  ↓
高级:理解闭包的底层原理
  ↓
专家:优化闭包性能,避免陷阱

参考资料