我们在开发中可能对闭包这个东西不是很重视,今天就带大家来温习一下这个让人头疼的玩意儿,看看它到底有多可恶。
setInterval中的闭包
案发现场
先来看一段代码:
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
};
return (
<>
<button onClick={handleClick}>click button</button>
<text>{count}</text>
</>
);
}
export default App;
这段代码的逻辑很简单,点击button
,触发handleClick
,然后启动了一个定时器,每1000ms让count
的值+1。
然后我们观察一下打印的count
和页面渲染的count
值是如何变化的?你猜猜结果是什么?
结果是打印的一直是0,页面输出的一直是1。为什么会是这样呢?不应该是打印和页面输出的都是count++以后的值吗?
案情分析
我们来分析一下原因。
你这段代码中,count
的打印值一直不变化是因为 setInterval
内部的 count
值是在首次调用 handleClick
时被捕获的,而不会随着状态更新而改变。这是因为 JavaScript 闭包的特性。
- 当你首次点击按钮时,
handleClick
函数被调用,setInterval
开始执行。 setInterval
内部的count
值是handleClick
被调用时的值(初始值为 0),而且由于闭包的关系,这个count
值不会随着组件的重新渲染而更新。- 所以,虽然
setCount
会更新count
的状态,但是setInterval
内部依然引用的是最初的count
值,并且这个值不会随着状态变化而更新。
结案
我们可以使用函数形式的 setCount
来解决这个问题。
大家可以参考react官方示例函数形式的用法:react.dev/reference/r…
这样修改以后,函数形式的 setCount
可以获取到最新的状态值:
const handleClick = () => {
setInterval(() => {
setCount(prevCount => {
console.log(prevCount + 1);
return prevCount + 1;
});
}, 1000);
};
setCount
的函数形式会接收当前状态的前一个值prevCount
,并返回更新后的值。- 这样,
count
的值在每次更新时都会递增并且正确输出。
通过这种方式,console.log(prevCount + 1)
将每秒打印出更新后的值,并且页面渲染的count也是更新后的值。
其他作案现场
在其他的一些闭包使用场景中,闭包陷阱经常存在,由于变量的作用域、生命周期等原因,导致代码行为与预期不一致的情况。
1. 循环中的闭包
在循环中创建闭包,闭包中的变量总是引用循环中的最后一个值,而不是当时循环的值。
function createButtons() {
const buttons = [];
for (var i = 0; i < 5; i++) {
buttons.push(function() {
console.log('Button', i);
});
}
return buttons;
}
const buttons = createButtons();
buttons[0](); // 打印 5,而不是 0
buttons[1](); // 打印 5,而不是 1
在上述代码中,i
是使用 var
声明的,它在整个函数作用域内是共享的。当循环结束时,i
的值变为 5,闭包引用的也是这个值。因此,无论点击哪个按钮,都会打印 5。
解决方案:使用 let
替代 var
,或者使用立即执行函数(IIFE)。
// 使用let
function createButtons() {
const buttons = [];
for (let i = 0; i < 5; i++) {
buttons.push(function() {
console.log('Button', i);
});
}
return buttons;
}
// 使用 IIFE:
function createButtons() {
const buttons = [];
for (var i = 0; i < 5; i++) {
(function(i) {
buttons.push(function() {
console.log('Button', i);
});
})(i);
}
return buttons;
}
2. 异步操作中的闭包
场景:在异步操作中使用闭包时,闭包捕获的变量值可能与预期不同,导致错误的结果。
示例代码:
function fetchData() {
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('Fetching data for index:', i);
}, 1000);
}
}
fetchData();
// 1秒后连续打印:Fetching data for index: 3
// Fetching data for index: 3
// Fetching data for index: 3
原因:因为 i
是用 var
声明的,所以它在每次循环中是同一个变量。在 setTimeout
执行时,i
的值已经是 3。
解决方案:同样可以使用 let
或 IIFE。
使用 let
:
function fetchData() {
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log('Fetching data for index:', i);
}, 1000);
}
}
3. 函数返回值中的闭包
场景:当一个函数返回一个闭包时,该闭包引用的外部变量在函数调用后仍然存在。
示例代码:
function createCounter() {
let count = 0;
return function() {
count++;
console.log('Count:', count);
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // Count: 1
counter1(); // Count: 2
counter2(); // Count: 1
原因:count
变量在 createCounter
函数作用域内,但闭包(返回的函数)仍然持有对 count
的引用。因此,每次调用 counter1
或 counter2
,它们都会更新并打印各自的 count
值。
解决方案:这是闭包的典型用法,不需要改变。但需小心管理好这些引用的生命周期,以防止意外内存泄漏或难以追踪的状态。
4. 嵌套函数中的闭包
场景:嵌套函数中的闭包可能会导致变量意外共享,产生错误结果。
示例代码:
function outer() {
let count = 0;
function inner() {
console.log(count);
}
count++;
return inner;
}
const fn = outer();
fn(); // 打印 1
原因:inner
函数持有对 outer
函数作用域中 count
变量的引用。当 outer
函数返回后,count
的值仍然保存在 inner
的闭包中。
总结
闭包是 JavaScript 中强大且常用的特性,但也容易导致陷阱。常见的闭包陷阱通常与变量的作用域、生命周期以及 JavaScript 的异步行为相关。理解这些场景并使用合适的方式(如 let
、IIFE、函数式编程)来规避陷阱,可以帮助你写出更健壮的代码。