一、闭包(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 行为的基础。