跳到主要内容
全部已验证问答

说一下你对闭包的理解 你知道函数执行栈吧,你如果闭包在里面会释放内存吗举了几个例子

作者已验证2026-08-14 · 回答由 AI 分身生成,作者本人盖章认可

先说结论:闭包不会让整个函数执行栈一直留在内存里。外层函数执行完后,执行上下文仍然会出栈;只有被内部函数引用、并且仍然可达的变量,才可能作为词法环境的一部分继续保留。等闭包本身不可达后,这些变量才能被垃圾回收。【1】【W4】

1. 什么是闭包

我对闭包的理解是:

函数和它创建时所在的词法环境(Lexical Environment)的组合。

简单说,内部函数即使离开了外部函数,仍然可以访问外部函数作用域里的变量,这就是闭包体现出来的能力。

function createCounter() {
  let count = 0

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

const counter = createCounter()

console.log(counter()) // 1
console.log(counter()) // 2

createCounter() 已经执行结束了,但 counter() 仍然能访问 count

原因不是 createCounter 的调用栈没有释放,而是返回的 counter 仍然引用着对应的词法环境,所以 count 仍然可达,暂时不会被垃圾回收【W4】。

可以简化为:

counter
   ↓
内部函数
   ↓
词法环境
   ↓
count = 2

2. 外层函数执行完,执行栈会不会释放

会释放。

调用函数时,会创建执行上下文并压入调用栈。函数执行结束后,对应的执行上下文会从调用栈中弹出。

function outer() {
  const value = 100

  function inner() {
    return value
  }

  return inner
}

const fn = outer()

调用过程可以简化为:

调用 outer()
    
调用栈:
┌──────────────┐
│ outer context│
├──────────────┤
│ global       │
└──────────────┘

outer() 返回之后:

调用栈:
┌──────────────┐
│ global       │
└──────────────┘

outer 的执行上下文已经出栈,但 fn 还引用着 inner,而 inner 又引用了 value 所在的词法环境。因此需要保留的变量会继续存在。

这里要避免一个常见的错误说法:

因为形成闭包,所以外层函数的调用栈没有释放。

这不准确。**释放的是调用栈上的执行上下文;被闭包捕获的数据会通过引擎内部的 Context、Environment 等结构继续存活。**从教学模型看,可以把它理解为相关变量被保存到堆中【1】【2】。具体如何存储属于 JavaScript 引擎实现细节,不应该机械地理解成“所有局部变量永远都在栈”或“整个作用域都复制到了堆”。

程序运行同时需要栈和堆,不可能把所有数据都直接放进栈中【3】【4】。

3. 哪些内存会被保留

通常只有闭包实际需要访问的变量有必要被保留。不过具体引擎可能因为优化策略,把一部分关联环境一起保留,不能只凭源码断言精确的底层内存布局。

function createGetter() {
  const used = { name: 'batype' }
  const unused = new Array(100000).fill('data')

  return function getName() {
    return used.name
  }
}

const getName = createGetter()

从语义上看:

  • used 被内部函数引用,需要保留;
  • unused 没有被引用,之后可以被回收;
  • createGetter 的执行上下文会正常出栈。

但在分析真实内存时,仍然要以浏览器 DevTools 的 Heap Snapshot 为准,因为引擎优化会影响实际表现。

4. 闭包什么时候释放

核心判断只有一个:闭包及其关联的词法环境是否仍然可达。

function createCounter() {
  let count = 0

  return () => ++count
}

let counter = createCounter()

console.log(counter()) // 1

counter = null

执行:

counter = null

之后,如果没有其他地方引用返回的函数,那么:

返回的函数 → 不可达
词法环境   → 不可达
count      → 可以被垃圾回收

注意是“可以被回收”,不是“这一行执行完立刻回收”。垃圾回收的具体时间由 JavaScript 引擎决定。词法环境只有在不可达时才会被清理【W4】。

5. 几个典型例子

例一:闭包一直被引用,变量不会释放

function createCache() {
  const cache = new Array(1000000).fill('data')

  return function getItem(index) {
    return cache[index]
  }
}

const getItem = createCache()

只要 getItem 一直可达,它引用的 cache 就必须保留。

console.log(getItem(0))

这不一定是内存泄漏,因为程序可能确实需要这个缓存。内存占用和内存泄漏不是一回事。

如果后续不再需要,应主动断开引用:

let getItem = createCache()

console.log(getItem(0))

getItem = null

例二:没有逃逸的闭包,执行后可以回收

function run() {
  const message = 'hello'

  function print() {
    console.log(message)
  }

  print()
}

run()

print 没有被返回,也没有注册到其他长期存活的对象中。

run() 执行结束后,如果没有其他引用:

  • run 的执行上下文出栈;
  • print 不再可达;
  • message 所在的环境可以被垃圾回收。

所以,出现嵌套函数不等于一定会长期占用内存。

例三:每次调用都会形成独立环境

function createCounter() {
  let count = 0

  return () => ++count
}

const counterA = createCounter()
const counterB = createCounter()

console.log(counterA()) // 1
console.log(counterA()) // 2

console.log(counterB()) // 1
console.log(counterB()) // 2

两次调用 createCounter(),会产生两套独立的词法环境:

counterA → countA
counterB → countB

因此两个计数器互不影响。多次调用外部函数并保存返回函数时,每次调用对应的词法环境都可能被保留【W4】。

例四:多个闭包共享同一个环境

function createCounter() {
  let count = 0

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

const counter = createCounter()

counter.increment()
counter.increment()
counter.decrement()

console.log(counter.getValue()) // 1

incrementdecrementgetValue 都是在同一次 createCounter() 调用中创建的,因此共享同一个 count

这也是闭包实现私有状态的一种常见方式。

例五:事件监听器导致闭包长期存活

function mount() {
  const largeData = new Array(1000000).fill('data')

  function handleClick() {
    console.log(largeData.length)
  }

  window.addEventListener('click', handleClick)

  return function unmount() {
    window.removeEventListener('click', handleClick)
  }
}

const unmount = mount()

引用关系是:

window
  ↓
handleClick
  ↓
largeData

即使 mount() 已经结束,window 仍然引用着 handleClick,因此 largeData 不能释放。

清理监听器:

unmount()

移除监听后,如果没有其他引用,handleClicklargeData 才可以被回收。

React 中的副作用清理,本质上也经常是在解决这类生命周期问题:

useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth)
  }

  window.addEventListener('resize', handleResize)

  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, [])

例六:定时器保留闭包

function startTimer() {
  const data = new Array(1000000).fill('data')

  const timerId = setInterval(() => {
    console.log(data.length)
  }, 1000)

  return () => {
    clearInterval(timerId)
  }
}

const stopTimer = startTimer()

只要定时器还在运行,回调就仍然可达,回调引用的 data 也会继续存在。

stopTimer()

取消定时器后,如果没有其他引用,相关内存才有机会被回收。

6. 闭包和异步回调的关系

异步回调经常会形成闭包,但闭包不等于异步。

function request() {
  const message = 'request finished'

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

request()

request() 很快执行结束并出栈,但定时器系统仍然持有回调函数。回调函数引用了 message,所以 message 在回调执行前不能被回收。

当定时器到期,回调会作为任务进入队列,等调用栈空闲后再执行。异步回调可以被安排为任务,也可以进入微任务队列;微任务会在当前任务末尾执行【7】。

7. var 循环中的经典闭包问题

const callbacks = []

for (var i = 0; i < 3; i++) {
  callbacks.push(function () {
    console.log(i)
  })
}

callbacks[0]() // 3
callbacks[1]() // 3
callbacks[2]() // 3

三个函数共享同一个 i。等函数真正执行时,循环已经结束,i 的值是 3

使用 let

const callbacks = []

for (let i = 0; i < 3; i++) {
  callbacks.push(function () {
    console.log(i)
  })
}

callbacks[0]() // 0
callbacks[1]() // 1
callbacks[2]() // 2

letfor 循环中会为每次迭代创建对应的词法绑定,因此三个闭包访问的是不同的 i

8. 闭包一定会造成内存泄漏吗

不会。

闭包只是保留词法环境的一种语言机制。只有当程序已经不需要某些数据,但仍存在意外引用导致数据长期可达时,才属于内存泄漏。

常见风险包括:

  • 未移除的事件监听器;
  • 未取消的定时器;
  • 长期存活的全局回调;
  • 无上限增长的缓存;
  • 已卸载组件仍被回调引用;
  • DOM 节点被闭包或其他对象继续引用。

因此判断闭包是否有问题,不应该问:

它有没有占内存?

而应该问:

这段数据已经不用了,为什么它仍然可达?

最后可以记住这三句话:

  1. 外层函数执行结束,执行上下文正常出栈。
  2. 被仍然可达的闭包引用的词法环境会继续存在。
  3. 闭包不可达后,相关环境才可以被垃圾回收,但回收时间由引擎决定。

对这个话题还有疑问?

接着问分身