Skip to content

闭包

闭包可以先理解成:函数在创建时,记住了自己所在的词法作用域,因此即使函数在外部执行,仍然能访问当时作用域里的变量。

闭包是怎么形成的

最常见的情况是:内部函数用到了外层函数的变量。

js
function createCounter() {
  let count = 0

  return function () {
    count++
    return count
  }
}

这里返回的函数在外部执行时,仍然可以访问 count,这就是闭包。

不要把闭包理解成“特殊语法”

闭包不是单独的语法点,而是函数和词法作用域一起工作的自然结果。

只要出现下面这类情况,就很容易形成闭包:

  • 函数返回另一个函数
  • 函数作为参数传出去后延后执行
  • 内部函数引用了外层变量

一个典型例子

js
function createAdder(base) {
  return function (value) {
    return base + value
  }
}

const add10 = createAdder(10)
console.log(add10(5)) // 15

add10 执行时,createAdder 早已运行结束,但 base 仍然可用。

为什么外层函数结束了,变量还在

因为只要返回出去的函数还在被引用,相关的词法环境就还不能被回收。

js
function createHandler() {
  const data = [1, 2, 3]

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

const handler = createHandler()

这里 data 是否能被回收,取决于 handler 是否还活着。

闭包最常见的使用场景

封装私有状态

js
function createCounter() {
  let count = 0

  return {
    increment() {
      count++
      return count
    },
    getCount() {
      return count
    },
  }
}

工厂函数

js
function createLogger(prefix) {
  return function (message) {
    console.log(`[${prefix}] ${message}`)
  }
}

回调延后执行

js
function setup() {
  const name = 'Lee'

  setTimeout(() => {
    console.log(name)
  }, 1000)
}

闭包和内存泄漏不是一回事

这是高频误区。

闭包本身不是内存泄漏。它只是让某些变量因为“后面还可能用到”而继续存活。

真正的问题是:

  • 数据其实已经不需要了
  • 但外部引用没有释放
  • 导致本该回收的内容一直留着

什么情况下要警惕

更需要注意的是这类情况:

  • 闭包里持有大对象
  • 闭包被长期挂在全局变量、定时器、事件监听器上
  • 本来不再需要,但引用一直没清理
js
function bind() {
  const largeData = new Array(1000000).fill(0)

  return function () {
    return largeData.length
  }
}

const handler = bind()

如果 handler 一直存在,largeData 就不会被释放。

怎么判断是不是闭包导致的问题

可以先顺着这个思路判断:

  1. 内部函数是否引用了外层变量
  2. 这个函数是否还被外部持有
  3. 外层变量是否其实已经不再需要

如果前三步都成立,就要考虑是否存在不必要的长期引用。

使用建议

  • 先把闭包理解成“函数记住外层作用域”,不要把它神秘化。
  • 需要私有状态、延迟执行、工厂函数时,闭包很自然。
  • 闭包里不要无意义持有大对象或 DOM 引用。
  • 排查内存问题时,重点看“引用是否还存在”,而不是只看“有没有闭包”。

基于 MIT 许可发布