深入理解 JavaScript 闭包

深入理解 JavaScript 闭包

什么是闭包?

闭包(Closure)是 JavaScript 中最核心也最容易被误解的概念之一。它不是某个 API,也不是 ES6 新增的语法特性,而是由语言本身的词法作用域机制自然产生的现象。理解闭包,意味着你真正理解了 JavaScript 的作用域链和执行上下文。

闭包的定义

闭包是指一个函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。

js 复制代码
function outer() {
  let count = 0
  function inner() {
    count++
    console.log(count)
  }
  return inner
}

const fn = outer()
fn() // 1
fn() // 2
fn() // 3

这段代码中,inner 被返回到了外部,并在 outer 执行完毕后继续被调用。每次调用 fn()count 都会自增------这就是闭包在起作用。

闭包的形成机制

词法作用域

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

js 复制代码
const x = 'global'

function foo() {
  console.log(x) // 指向全局 x
}

function bar() {
  const x = 'local'
  foo() // 打印 "global"
}

函数作为一等公民

JavaScript 中的函数可以像普通值一样被赋值、传递参数、作为返回值。这使得函数可以"逃离"其定义时的作用域,带着它的作用域链一起。

执行上下文与作用域链

每个函数调用都会创建自己的执行上下文,其中包含词法环境的引用。闭包的特殊之处在于:即使外层函数的执行上下文已弹出调用栈,其变量对象仍然被内层函数引用着,因此不会被垃圾回收。

核心应用场景

1. 数据私有化

js 复制代码
function createCounter() {
  let _count = 0

  return {
    increment: () => ++_count,
    decrement: () => --_count,
    getCount: () => _count,
  }
}

const counter = createCounter()
counter.increment() // 1
counter.increment() // 2
console.log(counter._count) // undefined

2. 函数工厂

js 复制代码
function multiply(a) {
  return function (b) {
    return a * b
  }
}

const double = multiply(2)
const triple = multiply(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15

3. 模块模式(IIFE)

js 复制代码
const myModule = (function () {
  const privateKey = 'secret'

  return {
    publicMethod() {
      console.log(privateKey)
    },
  }
})()

myModule.publicMethod()

4. 回调与事件处理

js 复制代码
function setupButton(buttonId) {
  let clickCount = 0

  document.getElementById(buttonId).addEventListener('click', function () {
    clickCount++
    console.log('按钮被点击了', clickCount, '次')
  })
}

闭包与内存

闭包会让变量无法被回收,如果滥用可能导致内存泄漏

js 复制代码
function createHeavyClosure() {
  const heavyData = new Array(1000000).fill('data')

  return function () {
    console.log('hello') // 没用到 heavyData
  }
}
// heavyData 永远不会被释放

解决:只保留需要的引用,或用完后手动解除。

循环中的经典问题

js 复制代码
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i) // 打印 5 次 5
  }, 1000)
}

修复方式:

  1. 使用 let
js 复制代码
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 1000)
}
  1. IIFE 闭包
js 复制代码
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 1000)
  })(i)
}

闭包与 React Hooks

Stale Closure

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1) // count 始终是 0
    }, 1000)
    return () => clearInterval(timer)
  }, [])
}

修复:使用函数式更新 setCount(prev => prev + 1)

useCallback 与闭包

jsx 复制代码
const handleClick = useCallback(() => {
  doSomething(dep)
}, [dep])

总结

闭包不是语言刻意设计的"特性",而是词法作用域与函数作为一等公民的自然结果。理解它的关键:

  • 函数定义时确定作用域链,而非调用时
  • 被返回的函数会"携带"其词法环境
  • 数据私有化、函数工厂、模块模式的底层支撑
  • 注意内存泄漏和循环中的闭包陷阱
  • React Hooks 的本质也离不开闭包
相关推荐
xy345319 小时前
axure9.0 如何打造一个计时器(简单版)
前端·ui·html·axure·原型·产品设计
IMPYLH19 小时前
HTML 的 <pre> 元素
前端·javascript·html
tsumikistep20 小时前
【前端】Vue + Axios 跨域问题实战:7070 代理转发 8080 + 401 报错分析(黑马外卖)
前端·javascript·vue.js
lichenyang45320 小时前
鸿蒙首页从等高商品 Grid 到双列瀑布流:同一张图也能做出小红书式浏览节奏
前端
里欧跑得慢20 小时前
Flutter主题与样式详解
前端·css·flutter·web
计算机魔术师20 小时前
TPU推理性价比翻50%,NVIDIA的护城河要见底了?
前端
IT_陈寒21 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
OpenTiny社区21 小时前
【直播分享】GenUI SDK 技术公开课第二讲 | GenuiChat 核心配置深度解析
前端·github
计算机魔术师21 小时前
kernel.org 每天 600 万次请求,98% 不是人点的
前端
今日无bug1 天前
从0到1手撕流式输出:Vue3 + Vite 实现 LLM 流式响应全解析
前端·vue.js·vite