深入理解 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 的本质也离不开闭包
相关推荐
hoLzwEge10 小时前
拯救低效调试!code-inspector-plugin 让代码定位如虎添翼
前端·前端框架·node.js
J船长10 小时前
烂笔头扫盲:LLM Eval 入门:从「感觉挺好」到「数据说话」的工程化落地
前端
skiyee10 小时前
被多个 UView 系 UI 库 “抄” 的 @uni-ku/root 究竟怎么实现?
前端
鱼毓屿御10 小时前
从「只会聊」到「边想边做」的跃迁
前端·学习·react.js
sugar__salt11 小时前
Vue3 表单双向绑定与响应式核心 API 技术详解
前端·javascript·vue.js
郝亚军11 小时前
使用Vue 3和Nginx打包和部署Vue.js项目的一般步骤
前端·vue.js·nginx
SmartBoyW11 小时前
CSS弹性布局(Flexbox)学习笔记:从零开始搞懂弹性布局
前端·javascript
用户693717500138411 小时前
从代码生产者到 AI 协作者:软件工程师的角色重构
android·前端·后端
嘟嘟071711 小时前
搞懂 useState 从原生 DOM 到 React 惰性初始化的完整链路
前端·react.js·编程语言
牧艺11 小时前
cos-design RippleWater & SmokeFog:水面涟漪与烟雾雾气怎么做
前端·canvas·视觉设计