函数、作用域与闭包
函数、作用域与闭包 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课通过实现计数器和防抖函数(debounce,把连续触发合并成最后一次执行)来理解作用域(scope,变量可被访问的范围)与闭包(closure,内层函数「记住」外层函数变量的现象)。读完你应该能解释回调为什么拿到「旧值」,以及这件事在什么条件下发生。
问题场景
搜索框想要「停止输入 500 毫秒后再发请求」。第一版直接在按键事件里发请求,每敲一键发一次;第二版想用定时器合并,但定时器 id 存在局部变量里,函数一返回变量就没了,上一次的定时器永远清不掉。要让「上一次的 id」活下来,你需要闭包。
核心解释
先看作用域:函数可以读取自己定义处外层一层层的变量,这条查找路径叫作用域链(scope chain)。当内层函数引用了外层函数的变量、并且外层函数返回后这个引用还活着——这就是闭包:
function makeCounter() {
let count = 0; // 只属于这一次 makeCounter 调用
return function () {
count += 1; // 内层函数捕获了 count
return count;
};
}
const c1 = makeCounter();
const c2 = makeCounter();
c1(); c1(); c1(); // 依次返回 1、2、3
c2(); // 返回 1 —— 两次调用各自持有一份 count
关键认识:闭包捕获的是「变量本身」,不是「那一刻的值」。c1 和 c2 是两次调用产生的两个 count,互不影响;而同一个闭包里的每次调用,看到的都是同一个、最新的 count。
防抖正是利用这一点:把定时器 id 关在闭包里,外部碰不到,但每次触发都能读到并清掉上一次的定时器:
function debounce(fn, wait) {
let timer = null; // 闭包变量:跨越多次调用存活
return function (...args) {
clearTimeout(timer); // 清掉上一次还没执行的
timer = setTimeout(() => fn(...args), wait);
};
}
示例拆解
把防抖接到搜索框上,观察「回调捕获旧值」的形成条件:
const log = debounce((text) => console.log('搜索:', text), 500);
input.addEventListener('input', (e) => log(e.target.value));
- 快速输入
react五个键:五次调用进入 debounce 返回的同一个函数,前四次刚设好的定时器都被下一次clearTimeout清掉,最终只打印一次搜索: react。 - 注意
(...args):参数也是靠闭包传递的。如果回调需要「触发那一刻」的值,必须在触发时通过参数传进去,而不是在回调里读一个外部变量——回调执行时那个外部变量可能已经变了(旧值问题)。 - 验证:把
wait改成 0,快速输入仍只打印一次——只要两次触发间隔小于一次事件循环,合并就成立。这说明防抖与「时间长短」无关,与「合并触发」有关(事件循环在 F09 展开)。
动手任务
打开本课实验区:
- 给 makeCounter 加一个
reset(),让c1.reset(); c1()返回 1。提示:返回一个带两个方法的对象,两个方法共享同一个闭包变量。 - 实验区里有一段「旧值演示」:先在注释里预测三次回调会打印什么,再运行对照,并解释原因。
- 进阶(可选):给 debounce 加「第一次立即执行」开关:
debounce(fn, wait, { leading: true })。
常见错误
- 以为闭包复制了值:闭包记住的是变量本身;变量后来变了,闭包里读到的就是新值——「回调拿旧值」和「回调读到新值」是同一个原理的两面。
- 在循环里用 var 声明闭包变量:var 没有块作用域,五次循环共享一个 i,回调全打出 5;改用 let,每轮循环产生新的 i。
- 防抖后丢了参数:返回的函数要用
...args转发参数,漏掉后回调收到的永远是 undefined。 - 闭包持有大对象不放:闭包会让外层变量一直活着;只在需要时捕获,用完置 null,避免内存降不下来。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能说出「闭包捕获的是变量,不是值」,并举例说明两种后果
- 能解释 c1 和 c2 的 count 为什么互不影响
- 能写出 debounce,并说清 timer 存在哪里、为什么外部访问不到
参考资料
- MDN:闭包 —— 概念定义与经典例子
- MDN:函数指南 —— 作用域链与函数的完整背景
- MDN:setTimeout 全局函数 —— 定时器 id 与清除机制
读完这一课了?
标记完成后,工作台会安排接下来的学习。