Skip to content

闭包与作用域

闭包是 JavaScript 最核心的特性之一,也是函数式编程、模块化、React Hooks 的底层基础。

作用域链

JavaScript 使用词法作用域(静态作用域)——函数的作用域在定义时确定,而非调用时。

js
const x = 'global'

function outer() {
  const x = 'outer'

  function inner() {
    const x = 'inner'
    console.log(x)  // 'inner'(就近原则)
  }

  function inner2() {
    console.log(x)  // 'outer'(向上查找)
  }

  inner()
  inner2()
}

outer()

作用域链查找过程:

inner2 查找 x:
  inner2 自身作用域 → 没有
  outer 作用域 → 找到 'outer' ✓
  (不会继续向上查找)

闭包的定义

闭包 = 函数 + 其词法环境(Lexical Environment)的引用

当一个函数能访问其定义时所在作用域的变量,即使该作用域已经执行完毕,这个函数就形成了闭包。

js
function makeCounter() {
  let count = 0  // 这个变量被闭包"捕获"

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

const counter = makeCounter()
// makeCounter 已执行完毕,但 count 仍然存活
counter.increment()
counter.increment()
console.log(counter.value())  // 2

闭包的内存模型

makeCounter 执行完毕后:

堆内存中:
┌─────────────────────────────┐
│  Closure Environment        │
│  { count: 2 }               │  ← 被三个方法引用,不会被 GC
└──────┬──────────────────────┘
       │ 引用
┌──────▼──────────────────────┐
│  { increment, decrement,    │
│    value }                  │  ← counter 变量持有
└─────────────────────────────┘

经典陷阱:循环中的闭包

js
// ❌ 经典错误
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出:3 3 3(所有回调共享同一个 i)

// ✅ 方案一:let(块级作用域,每次循环创建新绑定)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出:0 1 2

// ✅ 方案二:IIFE 创建独立作用域
for (var i = 0; i < 3; i++) {
  ;(function(j) {
    setTimeout(() => console.log(j), 0)
  })(i)
}
// 输出:0 1 2

// ✅ 方案三:bind 绑定参数
for (var i = 0; i < 3; i++) {
  setTimeout(console.log.bind(null, i), 0)
}

实用模式

模块模式(IIFE)

js
const userModule = (() => {
  // 私有变量,外部无法访问
  let _users = []
  let _nextId = 1

  // 私有方法
  function _validate(user) {
    return user.name && user.email
  }

  // 公开 API
  return {
    add(user) {
      if (!_validate(user)) throw new Error('Invalid user')
      _users.push({ ...user, id: _nextId++ })
    },
    getAll() {
      return [..._users]  // 返回副本,防止外部修改
    },
    count() {
      return _users.length
    }
  }
})()

userModule.add({ name: 'Alice', email: 'alice@example.com' })
console.log(userModule.count())  // 1
// _users 无法从外部访问

函数工厂

js
// 创建带有预设参数的函数
function multiply(factor) {
  return (number) => number * factor
}

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

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

// 实际应用:事件处理器工厂
function createHandler(type) {
  return function(event) {
    console.log(`${type} event:`, event.target)
    // 每个 handler 都记住了自己的 type
  }
}

button.addEventListener('click', createHandler('click'))
input.addEventListener('input', createHandler('input'))

记忆化(Memoization)

js
function memoize(fn) {
  const cache = new Map()  // 闭包捕获 cache

  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
  }
}

const expensiveCalc = memoize((n) => {
  console.log(`计算 ${n}...`)
  return n * n
})

expensiveCalc(4)  // 计算 4... → 16
expensiveCalc(4)  // 直接返回 16(缓存命中)
expensiveCalc(5)  // 计算 5... → 25

偏函数应用(Partial Application)

js
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs)
  }
}

function add(a, b, c) {
  return a + b + c
}

const add5 = partial(add, 2, 3)  // 预设 a=2, b=3
console.log(add5(10))  // 15

// 实际应用:API 请求封装
const apiRequest = partial(fetch, 'https://api.example.com')
apiRequest('/users')   // fetch('https://api.example.com', '/users')

React Hooks 中的闭包

React Hooks 大量使用闭包,理解闭包是避免 Hooks 陷阱的关键:

jsx
// ❌ 闭包陷阱:stale closure
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的 count 是闭包捕获的初始值 0,永远不会更新
      setCount(count + 1)  // 始终是 0 + 1 = 1
    }, 1000)
    return () => clearInterval(timer)
  }, [])  // 空依赖数组,effect 只运行一次

  return <div>{count}</div>
}

// ✅ 正确:使用函数式更新,不依赖闭包中的 count
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1)  // 使用最新的 prev
    }, 1000)
    return () => clearInterval(timer)
  }, [])

  return <div>{count}</div>
}

闭包与内存泄漏

js
// ⚠️ 潜在内存泄漏:大对象被闭包意外持有
function createLeak() {
  const bigData = new Array(1000000).fill('data')  // 大数组

  return function() {
    // 即使只用到 bigData.length,整个 bigData 都不会被 GC
    console.log(bigData.length)
  }
}

// ✅ 只保留需要的数据
function createNoLeak() {
  const bigData = new Array(1000000).fill('data')
  const length = bigData.length  // 只保留需要的值
  // bigData 可以被 GC 了

  return function() {
    console.log(length)
  }
}

总结

  • 闭包 = 函数 + 词法环境,让函数"记住"定义时的作用域
  • var 在循环中共享变量,用 let 或 IIFE 解决
  • 闭包是模块化、函数工厂、记忆化的基础
  • React Hooks 依赖闭包,注意 stale closure 问题
  • 避免在闭包中意外持有大对象导致内存泄漏

系统学习 Web 前端生态,深入底层架构