闭包可能是被解释得最多、也被误解得最多的 JavaScript 概念之一。

“函数内部返回函数”“可以访问外部变量”“会造成内存泄漏”,这些说法都不算错,但都不够解释真实代码里的问题。

我后来更愿意把闭包理解成:函数和它定义时的词法环境一起被打包走了。

闭包保存的是环境,不是快照

function createCounter() {
  let count = 0

  return () => {
    count += 1
    return count
  }
}

const next = createCounter()
next() // 1
next() // 2

createCounter 返回的箭头函数仍然能访问 count,而且访问的是同一个绑定,不是调用时复制的一份值。

这也是闭包能做私有状态的基础:

function createStore(initial) {
  let value = initial

  return {
    get: () => value,
    set: next => {
      value = next
    },
  }
}

外部拿不到 value,只能通过 get / set 操作。比起把所有状态都挂在对象上,这种方式能明确边界。

闭包也会带来过期状态

闭包不会自动读取“最新变量”,它读取的是自己作用域里的绑定。如果这个绑定没有更新,你拿到的就是旧值。

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

// 3 3 3

var 只有一个函数级绑定,三个回调共享同一个 i。改成 let 后,每次循环都会创建新的绑定:

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

// 0 1 2

React 里的 stale closure 也是同一个问题。useEffect 或事件处理器捕获的是创建时的变量,如果没有正确的依赖或更新方式,就会一直读到旧值。

柯里化是参数的分批提交

柯里化把一个接收多个参数的函数,变成一串接收单个参数的函数:

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

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }

    return (...next) => curried.apply(this, [...args, ...next])
  }
}

const curriedAdd = curry(add)
curriedAdd(1)(2)(3) // 6
curriedAdd(1, 2)(3) // 6

这里的 fn.length 是函数声明时的形参数量。遇到默认参数、剩余参数或可变参数时,这个简单实现就不够用了,需要显式传入参数个数。

柯里化的实用场景是复用配置:

const check = curry((pattern, value) => pattern.test(value))

const isPhone = check(/^1\d{10}$/)
const isEmail = check(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)

它不是为了把代码写得更“函数式”,而是把一部分参数提前固定,生成更小的工具函数。

不要为了柯里化而柯里化

如果一段逻辑只调用一次,拆成一层层函数只会增加阅读成本。

柯里化适合:

  • 配置固定的校验器、请求器
  • 组合函数时逐步传入上下文
  • 需要复用中间函数的场景

不适合:

  • 只有一次调用的业务代码
  • 参数含义本来就不清晰,硬拆成 a => b => c
  • 为了炫技而牺牲调试信息

闭包和内存

闭包会保留它引用的变量。这本身不是内存泄漏。

真正的问题是:这个闭包活得比你想的久,并且继续引用着已经不需要的大对象。

function attach(element) {
  const hugeData = createHugeData()

  element.addEventListener("click", () => {
    console.log(hugeData.length)
  })
}

如果元素被移除,但事件监听器没有解绑,闭包和 hugeData 都可能继续可达。解决方式不是“不要用闭包”,而是管理生命周期:

const controller = new AbortController()
element.addEventListener("click", handler, { signal: controller.signal })

controller.abort()

或者及时 removeEventListener、清理定时器、取消订阅。

闭包让代码更灵活,代价是你需要知道自己延长了哪些引用的生命周期。

最后

闭包不是“函数套函数”的语法游戏,柯里化也不是函数式编程的通行证。

它们真正有价值的地方,是让状态和配置有一个清晰的边界。边界清楚了,闭包会很好用;边界失控了,它才会变成内存和状态问题的来源。