文章

闭包的实际应用场景深度解析

深入解析闭包在防抖节流、模块化模式、循环变量保存与函数柯里化中的实战应用

闭包的实际应用场景深度解析

一句话概括

闭包是 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; }
  };
})();

总结与扩展

核心要点

  1. 防抖:等待 N 秒,事件停止才执行
  2. 节流:固定节奏执行,不受事件频率影响
  3. 模块化:IIFE + 闭包 = 私有变量
  4. 柯里化:逐步传递参数,生成特化函数

延伸学习方向

  • Lodash 源码:防抖节流的工业级实现
  • Redux 中间件:闭包在中间件设计中的应用
  • React Hooks:useState、useEffect 的闭包原理
  • 函数式编程:compose、pipe、curry 的组合

相关主题

本文由作者按照 CC BY 4.0 进行授权