闭包(closeure)

什么是闭包

闭包(Closure)是指有权访问另一个函数作用域中变量的函数。简单来说,当一个函数(内部函数)在另一个函数(外部函数)内部定义,并且被外部函数返回或传递到外部作用域时,即使外部函数已经执行完毕,内部函数依然能“记住”外部函数的变量环境。

闭包形成的三个核心条件:

  1. 函数嵌套函数
  2. 内部函数引用外部函数的变量
  3. 内部函数被返回或传递到外部作用域

主要用途

  1. 封装私有变量:模拟面向对象中的“私有属性”,避免全局污染。
  2. 保持状态:让变量始终保存在内存中,不会被垃圾回收机制回收。
  3. 实现函数柯里化:将多参数函数转化为单参数函数的链式调用。
  4. 模块化开发:创建独立的作用域,隔离变量。

简单示例:用闭包实现计数器

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()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.getCount()); // 输出: 2
console.log(counter.decrement()); // 输出: 1

示例解析

  • 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() { // 闭包持有 element
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; // 闭包记住第一个参数 a
};
}

// 使用:先固定第一个参数
const add5 = curriedAdd(5);
console.log(add5(3)); // 输出: 8
console.log(add5(10)); // 输出: 15

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;
}

// 公共 API(通过闭包访问私有变量)
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()); // 输出: 1
console.log(UserModule.users); // 输出: undefined(私有变量无法访问)

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; // 闭包保存定时器 ID
return function(...args) {
clearTimeout(timer); // 每次输入清除上一次定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

// 使用:搜索框输入防抖
const handleSearch = debounce((keyword) => {
console.log('搜索关键词:', keyword);
}, 500);

handleSearch('Java'); // 被清除
handleSearch('JavaScript'); // 500ms 后执行

4. 解决循环中的变量绑定问题

经典面试题:用 var 声明循环变量时,闭包可修复“变量共享”问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误示例:所有定时器共享同一个 i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 3, 3, 3
}

// 修复方法 1:用闭包包裹每次循环的 i
for (var i = 0; i < 3; i++) {
(function(j) { // j 是当前循环的 i 的副本
setTimeout(() => console.log(j), 100); // 输出: 0, 1, 2
})(i);
}

// 修复方法 2:用 let 块级作用域(更简单,本质也是闭包)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
}

总结

  • 内存管理:闭包通过“引用持有”让变量存活,但需手动释放避免泄漏。
  • 高级场景:柯里化(参数复用)、模块化(封装私有变量)、防抖节流(性能优化)、循环变量修复(作用域隔离)。

工程化实现

针对 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
// src/hooks/useDebounceThrottle.js
import { ref, watch, onUnmounted } from 'vue';

/**
* 防抖 Hook
* @param {Function} fn - 需要防抖的函数
* @param {number} delay - 延迟时间(ms)
* @param {boolean} immediate - 是否立即执行
*/
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;
}

/**
* 节流 Hook(时间戳实现)
* @param {Function} fn - 需要节流的函数
* @param {number} interval - 间隔时间(ms)
*/
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.debouncelodash.throttle,结合 Vue 3 的 watchEffectv-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
// src/hooks/useDebounceThrottle.js
import { useRef, useEffect, useCallback } from 'react';

/**
* 防抖 Hook
*/
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]);
}

/**
* 节流 Hook(时间戳 + 定时器结合实现)
*/
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]);
}

/**
* 防抖值 Hook(用于处理输入框值)
*/
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('');

// 使用防抖值:keyword 变化后 500ms 更新 debouncedKeyword
const debouncedKeyword = useDebouncedValue(keyword, 500);

// 模拟 API 请求
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('提交表单');
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('提交成功');
}, []);

// 使用 lodash 防抖:立即执行,2秒内屏蔽后续点击
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);
}, []);

// 使用节流:每 200ms 更新一次
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:最大等待时间(节流的另一种实现)

闭包(closeure)
https://cszy.top/20250622-closeure/
作者
csorz
发布于
2025年6月22日
许可协议