本节摘要:函数把一段逻辑打包成可命名的单元,参数是原料入口、返回值是成品出口;作用域链规定名字的查找路线,闭包则让函数随身携带出生地的变量。本节讲函数的声明与调用、参数的灵活用法、作用域链的查找规则与闭包的形成条件,看板的统计函数群在这一节成形。
没有函数的脚本是一地零件,函数把它们装成整机。装机的收益有三:逻辑有了名字(名字即注释)、能反复调用(改一处全站生效)、能单独测试(第 5 章测试的基础)。函数的接口只有两端——参数收原料、返回值交成品;不写返回值的函数只是"执行动作",写了返回值的函数才是"加工原料",两种用法都合法,但混在一起会让人猜不透某个函数到底该干嘛。
// 看板统计函数群的第一批成员 function countByStatus(orders, status) { let n = 0; for (const o of orders) { if (o.status === status) n += 1; } return n; // 出口:加工原料、交付数字 } function renderStats(orders) { console.log("待开工:", countByStatus(orders, "pending")); console.log("加工中:", countByStatus(orders, "running")); console.log("已完成:", countByStatus(orders, "done")); // 无返回值的函数:执行动作(打印),不交成品 } const today = [ { product: "凸轮轴", status: "running" }, { product: "导套", status: "pending" }, { product: "芯轴", status: "done" }, ]; renderStats(today); // 输出三行统计——同一份原料喂三次,函数复用的最小演示
// 参数的灵活用法:默认值、多参数与选项对象 function slaOf(priority, hours = 24) { // 默认参数:不传就按一天算 return priority === "urgent" ? hours / 24 : hours; } function register(product, qty, options = {}) { const { owner = "未指派", note = "" } = options; // 选项对象:参数多于三个时改用对象收集,调用处自带字段名 return { product, qty, owner, note }; } console.log(register("凸轮轴", 40, { owner: "陈师傅" })); // { product: "凸轮轴", qty: 40, owner: "陈师傅", note: "" } // 对比散参数调用 register("凸轮轴", 40, "陈师傅", ""): // 对象字段名让每个值自带含义,参数顺序不再是记忆负担
每个函数在出生时就锁定了一条查找路线:先查自己身上的局部名,再查出生地的外层名,一路向外直到全局。这条路线叫作用域链。理解它的直接收益是读懂两类现象:内层可以读外层(层层向外借工具),外层读不到内层(车间外听不见班组内的对讲频道)。

闭包不是需要刻意"学"的高级技巧,它是作用域链的自然副产品:函数不管走到哪,都随身带着出生地的变量背包。真正要学的是识别它(函数跨出出生地仍在用当地变量)与善用它(把状态藏进背包,不污染全局)。
// 闭包实战一:工单计数器——状态藏进背包 function createCounter(start) { let count = start; // 背包里的私有状态:外界摸不到 return function step() { count += 1; // 内层函数引用背包变量:闭包形成 return count; }; } const nextMo = createCounter(1042); // 从 1042 起编号 console.log(nextMo()); // 1043 console.log(nextMo()); // 1044:count 在两次调用之间活着——背包的作用 const nextPo = createCounter(1); // 另一个独立背包,互不干扰 console.log(nextPo()); // 2
// 闭包实战二:搜索框防抖——背包里存计时器 function debounce(fn, wait) { let timer = null; // 背包状态:当前待触发的计时器 return function (...args) { clearTimeout(timer); // 新输入到来:作废上一次的等待 timer = setTimeout(() => fn(...args), wait); // 等够毫秒数才真正执行——工单搜索每敲一键就查一次太吵 }; } const searchOrders = debounce(function (kw) { console.log("按关键词查询:", kw); }, 300); searchOrders("凸"); // 300 毫秒内又敲了下一次,本次作废 searchOrders("凸轮"); // 300 毫秒内又敲了下一次,本次作废 searchOrders("凸轮轴"); // 停手满 300 毫秒,真正执行查询 // 收益:连击三个键只发一次查询,4.6 节接上真实请求后价值翻倍
两段代码是闭包在真实前端的两大经典用工:私有状态(计数器把编号规则藏起来,谁也改不了起始数)与延迟执行管理(防抖器把计时器存在背包里,连续触发只认最后一次)。它们共同替代了老式做法——把状态挂到全局变量上——那是脚本一多就互相踩脚的根源。
函数本身也是值,可以当参数传给另一个函数——被传进去等合适时机再调用的函数叫回调。回调节省了"重复模式"的样板代码:遍历每个元素执行某段逻辑、事件发生时执行某段逻辑,模式由系统函数提供,差异由回调填入。
// 回调的两个日常形态 const orders = [ { product: "凸轮轴", qty: 40, status: "running" }, { product: "导套", qty: 120, status: "pending" }, ]; orders.forEach(function (o) { // 形态一:遍历回调 console.log(o.product, "数量", o.qty); }); setTimeout(function () { // 形态二:定时回调 console.log("整点刷新统计"); }, 1000); // 两处的函数都是"原料":模式(遍历、延时)是别人的, // 内容(打印什么、刷新什么)是你的
// 回调进阶:高阶函数封装通用模式 function whenUrgent(orders, action) { // 高阶函数:收函数做参数的函数 for (const o of orders) { if (o.priority === "urgent") action(o); } } whenUrgent(today, (o) => console.log("加急!", o.product)); whenUrgent(today, (o) => sendNotify(o.owner)); // 同一个筛子、不同的动作:模式与内容解耦, // 这正是 4.3 节数组方法链的思想源头
⚠️ 常见坑:在循环里创建闭包却引用循环变量。老关键字的循环变量被所有闭包共享,循环结束后全部拿到终值;用取项循环或块级声明,每轮循环各自成箱,问题自然消失——这也是 4.1 节劝退老关键字的深层原因之一。
💡 关键直觉:看到"函数里返回函数"或"函数被当参数传递",立刻意识到闭包正在发生:背包里装了什么状态、这个状态会活多久,是读懂这类代码的两问。
单元能打包了,下一节给单元准备原料:数组与对象,工单数据的两辆载重车。