闭包问题的本质是异步操作和事件处理函数,总是会捕获到定义的值,而不是最新的值。
闭包的定义:
当一个函数(内层函数)能够记住并访问其所在的词法作用域(外层函数的作用域),即使该外层函数已经执行完毕,那么这个“内层函数”和“它所引用的外部作用域中的变量”共同构成了一个闭包。
面试题
如何避免内存泄露?
- 一个很经典的场景就是一个监听器(还有setTimeout和setInterver),注册在了一个生命周期很长的dom上面,那么它就会一直监听,造成内存泄露。这个在return里面清除一下即可。
- 使用WeakMap和WeakSet,被它应用的键,也会被回收。
如果JS不提供闭包,你如何实现闭包?
- 闭包是天然存在的,不会不提供。但是这个问题可以理解成,如何将状态数据和函数绑定在一起,并保持状态的私有化和持久化?
- 使用class类来封装,private对象就行;
- 或者用全局注册表/外部store实现,比如类似于Jotai或者Zustand那样;
- Zustand是使用一个中心化的外部存储,用useExternal来防止撕裂;
- 状态独立于React本身,react只是作为消费这
- Jotai则是“去中心化”,使用微小的,原子化的状态管理,进行组合。
- Zustand是使用一个中心化的外部存储,用useExternal来防止撕裂;
- 闭包是天然存在的,不会不提供。但是这个问题可以理解成,如何将状态数据和函数绑定在一起,并保持状态的私有化和持久化?
通过useRef创建一个持久引用,使得他总是获取到最新的值。
ts
/** 用于解决useState的闭包问题 */
export function useLatest<T>(props: T) {
const current = useRef<T>(props);
current.current = props;
return current;
}
tsx
// 闭包问题示例
function Counter() {
const [count, setCount] = useState(0);
// 有闭包问题的写法
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // 永远是0,因为捕获了初始渲染时的count值
}, 2000);
return () => clearTimeout(timer);
}, []); // 依赖数组为空
// 使用useLatest解决闭包问题
const latestCount = useLatest(count);
useEffect(() => {
const timer = setTimeout(() => {
console.log(latestCount.current); // 正确获取最新count值
}, 2000);
return () => clearTimeout(timer);
}, []); // 依赖数组仍为空
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
追问:
面试官的延伸追问
“除了闭包,你还知道哪些常见的 JavaScript 内存泄漏场景?”(例如:意外的全局变量、被遗忘的定时器 setInterval、DOM 节点的循环引用等)
“你用过哪些工具来检测和定位内存泄漏?”(例如:Chrome DevTools 的 Performance 和 Memory 面板)
“let 和 const 的出现,在多大程度上影响了我们对闭包的使用和理解?”(块级作用域使得在循环中不再需要 IIFE 来创建闭包了)
“你能解释一下 V8 引擎是如何优化闭包的吗?”(涉及到上下文(Context)和闭包对象(Closure Object)的优化,这是一个加分项)