F06第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

函数、作用域与闭包

函数、作用域与闭包 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课通过实现计数器和防抖函数(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 展开)。

动手任务

打开本课实验区:

  1. 给 makeCounter 加一个 reset(),让 c1.reset(); c1() 返回 1。提示:返回一个带两个方法的对象,两个方法共享同一个闭包变量。
  2. 实验区里有一段「旧值演示」:先在注释里预测三次回调会打印什么,再运行对照,并解释原因。
  3. 进阶(可选):给 debounce 加「第一次立即执行」开关:debounce(fn, wait, { leading: true })。

常见错误

  • 以为闭包复制了值:闭包记住的是变量本身;变量后来变了,闭包里读到的就是新值——「回调拿旧值」和「回调读到新值」是同一个原理的两面。
  • 在循环里用 var 声明闭包变量:var 没有块作用域,五次循环共享一个 i,回调全打出 5;改用 let,每轮循环产生新的 i。
  • 防抖后丢了参数:返回的函数要用 ...args 转发参数,漏掉后回调收到的永远是 undefined。
  • 闭包持有大对象不放:闭包会让外层变量一直活着;只在需要时捕获,用完置 null,避免内存降不下来。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能说出「闭包捕获的是变量,不是值」,并举例说明两种后果
  • 能解释 c1 和 c2 的 count 为什么互不影响
  • 能写出 debounce,并说清 timer 存在哪里、为什么外部访问不到

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(浏览器页面脚本环境)。页面有 3 个按钮,依次点击第 1、2、3 个按钮,控制台分别输出什么?

const buttons = document.querySelectorAll('button'); // 页面上有 3 个按钮

for (var i = 0; i < 3; i++) {
  buttons[i].addEventListener('click', () => console.log(i));
}
// 依次点击第 1、2、3 个按钮

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...