一句话概括
闭包是 JavaScript 的”瑞士军刀”——防抖节流、模块化、缓存、函数组合,所有这些实用技巧的背后都是闭包。
背景
学会闭包的概念只是第一步,更重要的是在实际场景中运用闭包。
面试中,除了问”什么是闭包”,面试官更爱问:
- “闭包能解决什么问题?”
- “你能手写一个防抖/节流函数吗?”
- “如何用闭包实现私有变量?”
这些都是在考察闭包的实战能力。
概念与定义
闭包的四大实战场景
| 场景 | 核心原理 | 解决的问题 |
|---|
| 防抖节流 | 闭包保存状态/定时器 | 限制高频事件触发 |
| 模块化模式 | 闭包创建私有作用域 | 封装私有变量 |
| 循环变量保存 | 闭包捕获每次迭代的值 | for 循环异步问题 |
| 函数柯里化 | 闭包保存部分参数 | 函数复用与组合 |
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 防抖:闭包保存定时器状态
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:闭包保存时间戳
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
|
核心知识点拆解
1. 防抖(Debounce)
原理:事件触发后,等待 N 秒再执行。如果 N 秒内再次触发,则重新计时。
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
| function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用示例
const handleSearch = debounce((value) => {
console.log('搜索:', value);
// 实际调用搜索 API
}, 500);
// 输入框事件
input.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
|
2. 节流(Throttle)
原理:事件触发后,立即执行。然后在 N 秒内不再执行,直到 N 秒后可再次触发。
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
| function throttle(fn, interval = 300) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用示例:滚动事件
const handleScroll = throttle(() => {
console.log('滚动位置:', window.scrollY);
// 检测滚动方向、更新吸顶导航等
}, 200);
window.addEventListener('scroll', handleScroll);
// 使用示例:按钮防连点
const submit = throttle(() => {
console.log('提交订单');
// 调用提交 API
}, 1000);
button.addEventListener('click', submit);
|
3. 防抖 vs 节流的选择
| 场景 | 推荐 | 原因 |
|---|
| 搜索框输入 | 防抖 | 停止输入后才搜索 |
| 窗口 resize | 节流 | 持续响应但控制频率 |
| 表单验证 | 防抖 | 停止输入后验证 |
| 滚动加载 | 节流 | 持续触发但不漏掉数据 |
| 按钮防连点 | 节流 | 限制点击频率 |
实战案例
案例一:带立即执行选项的防抖
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 debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
// 立即执行
fn.apply(this, args);
}
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用:搜索框立即显示提示
const search = debounce((value) => {
console.log('搜索:', value);
}, 300, true);
input.addEventListener('input', (e) => search(e.target.value));
|
案例二:带取消功能的防抖
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
| function debounce(fn, delay) {
let timer = null;
const debounced = function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
// 取消函数
debounced.cancel = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
// 使用:表单提交时取消未完成的搜索
const search = debounce((value) => {
console.log('最终搜索:', value);
}, 500);
input.addEventListener('input', (e) => search(e.target.value));
// 用户点击提交时,取消搜索
form.addEventListener('submit', () => {
search.cancel();
console.log('提交表单');
});
|
案例三:节流带 trailing/leading 选项
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
| function throttle(fn, interval, options = {}) {
const { trailing = true, leading = false } = options;
let lastTime = 0;
let timer = null;
return function (...args) {
const now = Date.now();
// leading: 立即执行
if (!lastTime && leading) {
fn.apply(this, args);
lastTime = now;
}
if (now - lastTime >= interval) {
// trailing: 结束后再执行一次
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
} else if (!timer && trailing) {
// trailing: 设置定时器在 interval 后执行
timer = setTimeout(() => {
lastTime = leading ? Date.now() : 0;
timer = null;
fn.apply(this, args);
}, interval - (now - lastTime));
}
};
}
// 使用
const handleScroll = throttle(
(scrollY) => console.log('滚动:', scrollY),
200,
{ trailing: true, leading: false }
);
|
模块化模式
闭包实现私有变量
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
| // 方式1:IIFE 模块
const Counter = (() => {
let count = 0; // 私有变量
let step = 1; // 私有变量
return {
getCount: () => count,
increment: () => {
count += step;
return count;
},
decrement: () => {
count -= step;
return count;
},
setStep: (s) => { step = s; }
};
})();
// 使用
Counter.increment();
Counter.increment();
console.log(Counter.getCount()); // 2
Counter.setStep(2);
Counter.increment();
console.log(Counter.getCount()); // 4
// count 和 step 无法直接访问
// console.log(count); // ReferenceError
|
命名空间模式
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
| const App = (() => {
// 私有成员
let version = '1.0.0';
const config = { apiUrl: '/api' };
// 私有方法
function log(message) {
console.log(`[${version}] ${message}`);
}
// 公共 API
return {
// 状态(只读暴露)
getVersion: () => version,
// 方法
init: () => {
log('初始化');
fetch(config.apiUrl + '/init');
},
// 允许修改配置
setApiUrl: (url) => {
config.apiUrl = url;
log('API 地址已更新');
}
};
})();
App.init(); // [1.0.0] 初始化
App.setApiUrl('/api/v2'); // [1.0.0] API 地址已更新
|
函数柯里化
基础柯里化
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
| // 普通函数
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// 柯里化版本
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
// 参数够了,直接调用
return fn.apply(this, args);
} else {
// 参数不够,返回新函数等待接收剩余参数
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
|
柯里化的实际应用
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
| // 案例:创建特定配置的函数
const multiply = (a, b) => a * b;
const curriedMultiply = curry(multiply);
// 创建固定第一个参数的函数
const double = curriedMultiply(2);
const triple = curriedMultiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 案例:链式 API
const prop = (key) => (obj) => obj[key];
const map = (fn) => (arr) => arr.map(fn);
const filter = (fn) => (arr) => arr.filter(fn);
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
// 组合使用
const getNames = compose(
map(prop('name')),
filter((user) => user.age > 18)
);
const users = [
{ name: '张三', age: 20 },
{ name: '李四', age: 15 },
{ name: '王五', age: 22 }
];
console.log(getNames(users)); // ['张三', '王五']
|
缓存与记忆化
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
| // 记忆化函数:用闭包缓存计算结果
function memoize(fn) {
const cache = {}; // 闭包变量:缓存存储
return function (...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('命中缓存:', key);
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// 使用:记忆化递归(斐波那契数列)
const fibonacci = memoize(function (n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // 55(计算)
console.log(fibonacci(10)); // 55(命中缓存)
console.log(fibonacci(11)); // 89(计算)
|
💡 记忆口诀:防抖等停、节流限速、模块封装私有、柯里化保存参数。
💡 人话总结:
- 防抖:等用户停手才干活(搜索框)
- 节流:固定节奏干活,不受打扰(滚动监听)
- 模块化:用闭包造一个”盒子”,里面放私有变量
- 柯里化:把多参数函数拆成多个单参数函数,逐步喂参数
高频面试题解析
Q1:你能手写一个防抖函数吗?
参考答案:
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
| function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 扩展:支持立即执行
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args);
}
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
|
Q2:防抖和节流的区别是什么?分别在什么场景使用?
参考答案: | 区别 | 防抖 | 节流 | |——|——|——| | 执行时机 | 事件停止 N 秒后执行 | 固定间隔执行 | | 执行次数 | 最后一次 | 第一次 + 间隔后重复 | | 适用场景 | 搜索、验证、resize | 滚动、按钮点击、拖拽 |
Q3:如何用闭包实现一个计算器,支持私有变量和方法?
参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const Calculator = (() => {
let result = 0; // 私有变量
return {
add: (n) => { result += n; return result; },
subtract: (n) => { result -= n; return result; },
multiply: (n) => { result *= n; return result; },
divide: (n) => {
if (n === 0) throw new Error('除数不能为0');
result /= n;
return result;
},
getResult: () => result,
reset: () => { result = 0; }
};
})();
|
总结与扩展
核心要点
- 防抖:等待 N 秒,事件停止才执行
- 节流:固定节奏执行,不受事件频率影响
- 模块化:IIFE + 闭包 = 私有变量
- 柯里化:逐步传递参数,生成特化函数
延伸学习方向
- Lodash 源码:防抖节流的工业级实现
- Redux 中间件:闭包在中间件设计中的应用
- React Hooks:useState、useEffect 的闭包原理
- 函数式编程:compose、pipe、curry 的组合
相关主题