什么是闭包
闭包(Closure)是指有权访问另一个函数作用域中变量的函数。简单来说,当一个函数(内部函数)在另一个函数(外部函数)内部定义,并且被外部函数返回或传递到外部作用域时,即使外部函数已经执行完毕,内部函数依然能“记住”外部函数的变量环境。
闭包形成的三个核心条件:
- 函数嵌套函数
- 内部函数引用外部函数的变量
- 内部函数被返回或传递到外部作用域
主要用途
- 封装私有变量:模拟面向对象中的“私有属性”,避免全局污染。
- 保持状态:让变量始终保存在内存中,不会被垃圾回收机制回收。
- 实现函数柯里化:将多参数函数转化为单参数函数的链式调用。
- 模块化开发:创建独立的作用域,隔离变量。
简单示例:用闭包实现计数器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function createCounter() { let count = 0;
return { increment: function() { count++; return count; }, decrement: function() { count--; return count; }, getCount: function() { return count; } }; }
const counter = createCounter(); console.log(counter.increment()); console.log(counter.increment()); console.log(counter.getCount()); console.log(counter.decrement());
|
示例解析:
count 变量被封装在 createCounter 函数内部,外部无法直接访问。
- 返回的三个方法(
increment/decrement/getCount)形成闭包,共享对 count 的访问权。
- 每次调用
createCounter() 都会创建独立的闭包作用域,多个计数器之间互不干扰。
一、闭包的内存管理
闭包的核心特性是让外部函数的变量在函数执行完毕后依然存活,这与其内存机制密切相关。
1. 变量为何不被回收?
JavaScript 引擎通过标记清除算法管理内存:当函数执行时,会创建一个“作用域链对象”(包含变量对象);若内部函数被返回并在外部被引用,引擎会认为“这些变量仍在被使用”,因此不会回收外部函数的作用域对象。
关键理解:闭包保存的是变量的引用,而非变量的快照。
2. 内存泄漏风险与避免
闭包可能导致内存泄漏,常见场景:
- 闭包持有 DOM 元素引用(如事件回调)
- 循环引用(闭包引用外部对象,外部对象又引用闭包)
避免方法:不再使用时手动释放引用
1 2 3 4 5 6 7 8 9 10 11
| function createLeak() { const element = document.getElementById('app'); element.onclick = function() { console.log(element.id); }; return element; }
let el = createLeak(); el = null;
|
二、闭包的高级应用场景
1. 函数柯里化(Currying)
将多参数函数转化为“单参数函数链”,实现参数复用和延迟执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function add(a, b) { return a + b; }
function curriedAdd(a) { return function(b) { return a + b; }; }
const add5 = curriedAdd(5); console.log(add5(3)); console.log(add5(10));
|
2. 模块化开发(IIFE + 闭包)
通过立即执行函数表达式(IIFE)创建私有作用域,模拟“类”的封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const UserModule = (function() { let users = [];
function validateUser(user) { return user.name && user.age > 0; }
return { addUser: function(user) { if (validateUser(user)) { users.push(user); console.log('用户添加成功'); } }, getUserCount: function() { return users.length; } }; })();
UserModule.addUser({ name: '张三', age: 25 }); console.log(UserModule.getUserCount()); console.log(UserModule.users);
|
3. 防抖(Debounce)与节流(Throttle)
闭包用于保存定时器状态,实现高频操作的性能优化。
防抖示例(输入停止后才执行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function debounce(fn, delay = 300) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }
const handleSearch = debounce((keyword) => { console.log('搜索关键词:', keyword); }, 500);
handleSearch('Java'); handleSearch('JavaScript');
|
4. 解决循环中的变量绑定问题
经典面试题:用 var 声明循环变量时,闭包可修复“变量共享”问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); }
for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); }
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); }
|
总结
- 内存管理:闭包通过“引用持有”让变量存活,但需手动释放避免泄漏。
- 高级场景:柯里化(参数复用)、模块化(封装私有变量)、防抖节流(性能优化)、循环变量修复(作用域隔离)。
工程化实现
针对 Vue 3 (Composition API) 和 React (Hooks) 提供工程化的防抖/节流实现,包括自定义 Hooks、第三方库集成(如 lodash)以及真实业务场景的完整代码示例。
一、Vue 3 (Composition API) 工程化实现
1. 自定义防抖/节流 Hooks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import { ref, watch, onUnmounted } from 'vue';
export function useDebounce(fn, delay = 500, immediate = false) { const timer = ref(null);
const debouncedFn = function(...args) { const context = this; if (timer.value) clearTimeout(timer.value);
if (immediate) { const canExecute = !timer.value; timer.value = setTimeout(() => { timer.value = null; }, delay); if (canExecute) fn.apply(context, args); } else { timer.value = setTimeout(() => { fn.apply(context, args); }, delay); } };
onUnmounted(() => { if (timer.value) clearTimeout(timer.value); });
return debouncedFn; }
export function useThrottle(fn, interval = 300) { const lastTime = ref(0);
const throttledFn = function(...args) { const now = Date.now(); const context = this; if (now - lastTime.value >= interval) { fn.apply(context, args); lastTime.value = now; } };
return throttledFn; }
|
2. 场景一:搜索框防抖(Vue 3 组件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <template> <div class="search-box"> <input v-model="keyword" placeholder="输入关键词搜索..." /> <div v-if="loading">搜索中...</div> <div v-else-if="result">搜索结果: {{ result }}</div> </div> </template>
<script setup> import { ref } from 'vue'; import { useDebounce } from '@/hooks/useDebounceThrottle';
const keyword = ref(''); const loading = ref(false); const result = ref('');
// 模拟 API 请求 const fetchSearch = async (val) => { if (!val) return; loading.value = true; try { // 模拟网络请求 await new Promise(resolve => setTimeout(resolve, 800)); result.value = `关于"${val}"的搜索结果`; } finally { loading.value = false; } };
// 使用防抖:停止输入 500ms 后执行搜索 const debouncedSearch = useDebounce(fetchSearch, 500);
// 监听 keyword 变化 watch(keyword, (newVal) => { debouncedSearch(newVal); }); </script>
|
3. 场景二:滚动加载节流(Vue 3 组件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <template> <div class="list-container"> <div v-for="item in list" :key="item.id" class="list-item"> {{ item.text }} </div> <div v-if="loading" class="loading">加载更多...</div> </div> </template>
<script setup> import { ref, onMounted, onUnmounted } from 'vue'; import { useThrottle } from '@/hooks/useDebounceThrottle';
const list = ref([]); const loading = ref(false); let page = 1;
// 模拟加载数据 const loadMore = async () => { if (loading.value) return; loading.value = true; try { await new Promise(resolve => setTimeout(resolve, 1000)); const newItems = Array.from({ length: 5 }, (_, i) => ({ id: page * 5 + i, text: `第 ${page * 5 + i} 条数据` })); list.value.push(...newItems); page++; } finally { loading.value = false; } };
// 检查是否滚动到底部 const checkScroll = () => { const scrollTop = document.documentElement.scrollTop; const clientHeight = document.documentElement.clientHeight; const scrollHeight = document.documentElement.scrollHeight; if (scrollTop + clientHeight >= scrollHeight - 100) { loadMore(); } };
// 使用节流:每 300ms 检查一次滚动 const throttledCheck = useThrottle(checkScroll, 300);
onMounted(() => { loadMore(); // 初始加载 window.addEventListener('scroll', throttledCheck); });
onUnmounted(() => { window.removeEventListener('scroll', throttledCheck); }); </script>
<style scoped> .list-item { height: 80px; border: 1px solid #eee; margin: 10px; padding: 10px; } .loading { text-align: center; padding: 20px; } </style>
|
4. 使用 lodash 简化实现(推荐工程化方案)
实际项目中更推荐直接使用成熟的 lodash.debounce 和 lodash.throttle,结合 Vue 3 的 watchEffect 或 v-model 使用:
1
| npm install lodash.debounce lodash.throttle
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <input v-model="keyword" placeholder="搜索..." /> </template>
<script setup> import { ref, watch, onUnmounted } from 'vue'; import debounce from 'lodash.debounce';
const keyword = ref('');
const doSearch = (val) => { console.log('搜索:', val); };
// 使用 lodash 防抖 const debouncedSearch = debounce(doSearch, 500);
watch(keyword, (newVal) => { debouncedSearch(newVal); });
// 组件卸载时取消防抖 onUnmounted(() => { debouncedSearch.cancel(); }); </script>
|
二、React (Hooks) 工程化实现
1. 自定义防抖/节流 Hooks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import { useRef, useEffect, useCallback } from 'react';
export function useDebounce(fn, delay = 500, deps = []) { const timer = useRef(null);
useEffect(() => { return () => { if (timer.current) clearTimeout(timer.current); }; }, []);
return useCallback((...args) => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => { fn(...args); }, delay); }, [fn, delay, ...deps]); }
export function useThrottle(fn, interval = 300, deps = []) { const lastTime = useRef(0); const timer = useRef(null);
useEffect(() => { return () => { if (timer.current) clearTimeout(timer.current); }; }, []);
return useCallback((...args) => { const now = Date.now(); const remaining = interval - (now - lastTime.current);
if (remaining <= 0) { if (timer.current) { clearTimeout(timer.current); timer.current = null; } lastTime.current = now; fn(...args); } else if (!timer.current) { timer.current = setTimeout(() => { lastTime.current = Date.now(); timer.current = null; fn(...args); }, remaining); } }, [fn, interval, ...deps]); }
export function useDebouncedValue(value, delay = 500) { const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(timer); }, [value, delay]);
return debouncedValue; }
|
2. 场景一:搜索框防抖(React 组件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import { useState, useEffect } from 'react'; import { useDebouncedValue } from '@/hooks/useDebounceThrottle';
function SearchBox() { const [keyword, setKeyword] = useState(''); const [loading, setLoading] = useState(false); const [result, setResult] = useState('');
const debouncedKeyword = useDebouncedValue(keyword, 500);
const fetchSearch = async (val) => { if (!val) return; setLoading(true); try { await new Promise(resolve => setTimeout(resolve, 800)); setResult(`关于"${val}"的搜索结果`); } finally { setLoading(false); } };
useEffect(() => { fetchSearch(debouncedKeyword); }, [debouncedKeyword]);
return ( <div className="search-box"> <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="输入关键词搜索..." /> {loading && <div>搜索中...</div>} {!loading && result && <div>{result}</div>} </div> ); }
export default SearchBox;
|
3. 场景二:按钮防重复点击(React 组件 + lodash)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { useCallback } from 'react'; import debounce from 'lodash.debounce';
function SubmitButton() { const handleSubmit = useCallback(async () => { console.log('提交表单'); await new Promise(resolve => setTimeout(resolve, 1500)); console.log('提交成功'); }, []);
const debouncedSubmit = useCallback( debounce(handleSubmit, 2000, { leading: true, trailing: false }), [handleSubmit] );
return ( <button onClick={debouncedSubmit}> 提交(2秒内只能点击一次) </button> ); }
export default SubmitButton;
|
4. 场景三:窗口 resize 节流(React 组件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import { useState, useEffect } from 'react'; import { useThrottle } from '@/hooks/useDebounceThrottle';
function WindowResize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
const handleResize = useCallback(() => { setSize({ width: window.innerWidth, height: window.innerHeight }); console.log('窗口尺寸更新:', window.innerWidth, window.innerHeight); }, []);
const throttledResize = useThrottle(handleResize, 200);
useEffect(() => { window.addEventListener('resize', throttledResize); return () => { window.removeEventListener('resize', throttledResize); }; }, [throttledResize]);
return ( <div> <h3>窗口尺寸</h3> <p>宽度: {size.width}px</p> <p>高度: {size.height}px</p> </div> ); }
export default WindowResize;
|
三、工程化最佳实践总结
1. 选型建议
| 场景 |
推荐方案 |
理由 |
| 简单项目/学习 |
自定义 Hooks |
轻量,理解原理 |
| 中大型项目 |
lodash.debounce/throttle |
边界情况处理完善,社区支持好 |
| React 输入框 |
useDebouncedValue |
专门处理值的防抖,逻辑清晰 |
| Vue 3 事件处理 |
watch + 防抖函数 |
结合响应式系统,使用灵活 |
2. 注意事项
- 内存泄漏:务必在组件卸载时清除定时器(Vue 的
onUnmounted、React 的 useEffect cleanup)。
- 闭包陷阱:React 中使用
useCallback 包裹函数,确保依赖项正确。
- lodash 配置:
leading: true:立即执行(适合按钮防重复)
trailing: false:不执行最后一次(避免重复提交)
maxWait:最大等待时间(节流的另一种实现)