React Hooks 与闭包

一、闭包(Closure)

1. 什么是闭包

闭包 = 自由变量的集合,在外部函数销毁后,内部函数仍能访问这些变量。

核心原理:词法作用域规则与"函数执行完毕上下文会被销毁"的规则冲突,JS 引擎将内部函数需要的变量放到堆内存中持久化保存。

2. 代码示例解析

示例1:基础闭包(closure/1.js)

javascript 复制代码
function foo() {
  function bar() {
    var a = 1
    console.log(b);  // 引用外部变量 b
  }
  var b = 2;
  return bar
}

const baz = foo()  // 返回 bar,foo 执行完毕但 b 被保留
baz();  // 输出 2

关键点 :bar 被返回后,foo 虽然执行完毕,但变量 b 不会被销毁,因为 bar 通过闭包引用了它。


示例2:函数记忆/计数器(closure/2.js)

javascript 复制代码
function Add(){
    let num = 0;
    return function(){
        num++; 
        return num;
    }
}
const res = Add()  // 创建独立作用域,num=0 被保存
console.log(res());  // 1
console.log(res());  // 2

关键点 :闭包可以保存状态,实现函数记忆。每次调用 res() 都会基于上一次的 num 值累加。


示例3:词法作用域(closure/3.js)

javascript 复制代码
function foo(){
    var myName = '极客邦'
    bar()
}
function bar(){
    console.log(myName);  // 输出什么?
}
var myName = '极客时间'
foo()

关键点 :输出 极客时间。词法作用域是静态 的,由函数声明位置决定。bar 声明在全局作用域,所以它查找 myName 时找到的是全局变量,而不是 foo 内部的。


示例4:闭包封装(closure/4.js)

javascript 复制代码
function foo(){
    var myName = '极客时间'
    let test1 = 1;
    const test2 = 2;

    var innerBar = {
        getName:function(){
            console.log(test1);  // 1
            return myName
        },
        setName:function(newName){
            myName = newName  // 通过闭包修改外部变量
        }
    }
    return innerBar
}

let bar = foo()   
bar.setName('极客邦')
console.log(bar.getName());  // 极客邦

关键点 :闭包可以实现数据封装,通过 setName 修改内部变量,通过 getName 获取。

3. 闭包的垃圾回收

场景 回收策略
闭包引用函数是全局变量 一直存在直到页面关闭,可能内存泄漏
闭包引用函数是局部变量 函数销毁后,JS 引擎判断回收

4. 作用域链与词法作用域

作用域链:每个执行上下文的变量环境中包含一个外部引用(outer),指向外部执行上下文。变量查找时沿着当前上下文 → outer → outer → ... → 全局的路径查找。

词法作用域 :作用域由代码中函数声明的位置决定,是静态的。词法作用域在代码编译阶段就决定好了,和函数如何调用无关。


二、React Hooks

1. useState 惰性初始化

javascript 复制代码
function heavyCalc() {
  console.log('🔥 执行昂贵计算');
  let sum = 0;
  for(let i = 0; i < 1000000; i++) sum += i;
  return sum;
}

export default function App() {
  // ✅ 惰性初始化:传入函数,仅挂载时运行一次
  const [value, setValue] = useState(() => heavyCalc());

  // ❌ 普通写法:每次重渲染都会调用 heavyCalc()
  // const [value, setValue] = useState(heavyCalc());

  return (
    <div>
      <p>结果:{value}</p>
      <button onClick={() => setValue(v => v + 1)}>点我更新state</button>
    </div>
  );
}

核心要点:

  • 传入函数 → 惰性初始化,只在首次挂载时执行
  • 传入值 → 每次渲染都会执行计算,浪费性能

2. useState 闭包陷阱

javascript 复制代码
const App = () =>{
  const [count, setCount] = useState(0);
  
  useEffect(() =>{
    const timer = setInterval(()=>{
      // ❌ 闭包陷阱:count 永远是 0
      // setCount(count + 1)
      
      // ✅ 函数式更新:拿到最新值
      setCount(pervCount => pervCount + 1)
    }, 1000) 
    return () => clearInterval(timer)
  }, [])  // 空依赖数组
  
  return <div>{count}</div>
}

闭包陷阱详解:

  • useEffect 的依赖数组为空 [],所以回调只在挂载时执行一次
  • setInterval 的回调函数捕获了当时的 count = 0
  • 如果用 setCount(count + 1),每次都是 0 + 1 = 1,计数器失效
  • 解决方案 :使用函数式更新 setCount(prev => prev + 1),让 React 传入最新值

3. useState 批量更新

javascript 复制代码
const handleClick = () =>{
  // ❌ 结果是 1,不是 3
  // 批量更新会异步合并
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  
  // ✅ 函数式更新,结果是 3
  setCount((prevCount) => prevCount + 1)
}

核心要点:

  • React 会批量合并多次 state 更新,减少渲染次数
  • 直接传值会基于当前渲染周期的旧值
  • 函数式更新确保基于最新值计算

三、闭包与 Hooks 的关系

React 函数组件每次渲染都是一次函数调用,Hooks 利用闭包来保存状态:

markdown 复制代码
组件渲染 → 函数执行 → Hooks 通过闭包捕获当前状态
        ↓
    状态更新 → 重新渲染 → 新的闭包捕获新状态
概念 说明
闭包 函数能访问其声明时的词法作用域变量
Hooks 闭包陷阱 回调捕获了旧渲染周期的状态快照,导致拿到过期值
解决方案 函数式更新 setState(prev => ...)

这就是为什么 React Hooks 经常和闭包一起讨论------理解闭包是理解 Hooks 行为的基础。

相关推荐
excel14 分钟前
研究 Vue 3 源码的收获
前端·vue.js
可乐鸡翅yeah_2 小时前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线
lerhxx2 小时前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
计算机魔术师2 小时前
METR 演示 AI 智能体如何篡改 Inspect 评估记录以掩盖不当行为
前端
前端snow3 小时前
ai agent --- 异步处理之 Rabbit MQ
前端
念何架构之路3 小时前
zap扩展生态与总结
java·前端·数据库
独孤九剑打醒他3 小时前
【原创开源】【概念设计】源 - 栅 - 漏 - 栅 - 源 横向双栅 MOS,低压交流多值逻辑芯片探索
前端·其他·架构·开源·硬件工程
mantou1324 小时前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端
默_笙4 小时前
🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
前端·javascript
纸片人4 小时前
Blender 建模 + Three.js 展示:和 AI 一起做一个光储充超充站数字孪生大屏
前端