函数与回调
上一节能遍历学员并计算结果。如果多个地方都需要成绩等级和学员摘要,复制判断代码会让修改规则变得困难。本节先把单个任务封装成函数,再把函数作为参数传入批量处理流程,为下一节的数组方法做准备。
函数通过参数接收输入,通过 return 返回结果。先明确“传入什么、得到什么”,再编写内部步骤。
函数基础
声明、调用与参数
先用最短的欢迎语练习声明和调用,再把重复的成绩处理写成函数。
function greetStudent(studentName) {
console.log(`欢迎 ${studentName} 加入课程`);
}
greetStudent("张三");
greetStudent("李四");函数声明本身不会执行函数体,使用函数名加括号才是调用。studentName 是形参,"张三" 是实参。
多个参数:
function formatStudent(studentName, courseName) {
return `${studentName} · ${courseName}`;
}
console.log(formatStudent("张三", "FastAPI Web 全栈"));参数过多时通常考虑传入对象,让字段含义更清楚。
返回值
下面的成绩已按前两节的规则校验,getLevel() 只负责把有效成绩转换为等级。
function getLevel(score) {
if (score >= 90) {
return "优秀";
}
if (score >= 80) {
return "良好";
}
if (score >= 60) {
return "合格";
}
return "需要补考";
}
const level = getLevel(86);
console.log(level);return 把结果交给调用位置,并立即结束当前函数。没有显式 return 的函数返回 undefined。
优先让计算函数返回结果,由调用者决定是输出、保存还是继续计算:
function calculateAverage(total, count) {
if (count === 0) {
return 0;
}
return total / count;
}
console.log(calculateAverage(251, 3));参数约定
默认参数
function formatStatus(status = "pending") {
if (status === "active") {
return "正常";
}
if (status === "pending") {
return "待审核";
}
if (status === "disabled") {
return "已停用";
}
return "未知";
}
console.log(formatStatus()); // "待审核"
console.log(formatStatus("archived")); // "未知"
console.log(formatStatus(null)); // "未知"只有实参为 undefined 或没有传入时才使用默认值;传入 null 不会触发默认参数。未知状态也不能当作“待审核”,应与综合案例一样返回“未知”。
输入校验
下面把前面的 calculateAverage() 改为带校验的版本。两个代码块分别运行,避免同名函数混在一起。
function calculateAverage(total, count) {
if (!Number.isFinite(total) || !Number.isInteger(count) || count < 0) {
return null;
}
if (count === 0) {
return 0;
}
return total / count;
}
console.log(calculateAverage(251, 3)); // 约 83.67
console.log(calculateAverage(0, 0)); // 0
console.log(calculateAverage(NaN, 2)); // null这里约定总分必须是有限数字,人数必须是非负整数;无人时以 0 表示平均分,无效参数返回 null。typeof NaN 也是 "number",所以只检查类型不够。大型项目会有更完整的错误约定,本节先保持简单。
作用域与数据共享
函数与块级作用域
const courseName = "FastAPI Web 全栈";
function createSummary(studentName) {
const message = `${studentName} · ${courseName}`;
return message;
}
console.log(createSummary("张三"));
// console.log(message); // ReferenceError- 外层变量在函数内部可见。
- 函数内部声明的变量只在函数内部可见。
- 参数也是函数局部变量。
函数需要的数据优先通过参数传入,避免读取和修改大量全局变量。
除了函数,if、for 等语句的花括号也会形成代码块。let 和 const 声明的变量只在所在代码块及其内部可见:
const score = 86;
if (score >= 60) {
const resultText = "合格";
console.log(resultText); // 可以读取
}
// console.log(resultText); // ReferenceError:超出块级作用域
for (let index = 0; index < 2; index += 1) {
console.log(index); // 依次输出 0、1
}
// console.log(index); // ReferenceError如果分支结束后还要使用某个结果,应先在外层声明,再在分支中赋值。前面成绩分级示例把 let level 写在 if 之前,就是为了在判断结束后输出它。
内外层可以声明同名变量,但它们是不同的绑定:
const status = "active";
{
const status = "pending";
console.log(status); // "pending",优先读取当前层
}
console.log(status); // "active",外层变量没有被修改这种同名遮蔽容易让初学者误以为外层数据被修改,业务代码优先使用清楚的名称。同一个作用域不能重复用 let、const 声明同名变量,也不能在它们初始化之前读取。
参数与原数据
数字、字符串等值作为参数传入后,给形参重新赋值不会改变调用处的变量。对象参数仍指向原对象,直接修改其属性会影响调用者的数据。
function changeScore(score) {
score = 90;
return score;
}
const originalScore = 86;
console.log(changeScore(originalScore)); // 90
console.log(originalScore); // 86对象参数的属性修改则会留在原对象上:
function disableStudent(student) {
student.status = "disabled";
}
const student = {name: "张三", status: "active"};
disableStudent(student);
console.log(student.status); // "disabled"本章的摘要函数读取输入字段,再返回新对象;需要改变原数据的函数,应让名称和说明明确表达这个行为。下一节会用展开语法学习创建更新后的对象。
函数作为值
函数表达式与赋值
函数也可以作为值保存:
const getLevel = function (score) {
return score >= 60 ? "合格" : "需要补考";
};
console.log(getLevel(86));函数声明在当前作用域解析时即可使用;函数表达式需要等变量完成初始化后再调用。基础项目在普通命名函数中优先使用函数声明,回调中会常见函数表达式或箭头函数。
函数可以像数据一样赋给另一个变量;下一小节再把它作为参数传入:
function formatName(student) {
return student.name;
}
const formatter = formatName;
console.log(formatter({name: "张三"}));这里没有写 formatName(),因为我们要传递函数本身,不是立即调用后的结果。
回调与筛选
传给另一个函数、由对方在合适时机调用的函数称为回调:
function processStudent(student, formatter) {
const result = formatter(student);
console.log(result);
}
function formatStudentName(student) {
return `学员:${student.name}`;
}
const student = {id: 1, name: "张三"};
processStudent(student, formatStudentName);执行过程:
processStudent 接收数据和函数
↓
内部调用 formatter(student)
↓
formatStudentName 产生字符串
↓
processStudent 输出结果回调让“处理流程”和“具体处理方式”分开。
回调不等于异步。上例在 processStudent() 执行过程中立即调用 formatter(),属于同步调用;后续事件和请求中的回调会结合具体运行时机再讲解。
案例:手写一个筛选函数
function filterStudents(students, predicate) {
const result = [];
for (const student of students) {
if (predicate(student)) {
result.push(student);
}
}
return result;
}
function isActiveStudent(student) {
return student.status === "active";
}
const students = [
{name: "张三", status: "active"},
{name: "李四", status: "disabled"},
{name: "王五", status: "active"},
];
const activeStudents = filterStudents(students, isActiveStudent);
console.table(activeStudents);下一节的 Array.prototype.filter() 就采用相同思想。先看懂手写版本,再学习内置方法会更自然。
学员摘要生成
本例约定每条学员记录的成绩已校验为 0 到 100 之间的数字。先给单条记录生成摘要,再把这个函数作为参数交给遍历函数。
function getScoreLevel(score) {
if (score >= 90) {
return "优秀";
}
if (score >= 80) {
return "良好";
}
if (score >= 60) {
return "合格";
}
return "需要补考";
}
function getStatusText(status) {
const statusTextMap = {
active: "正常",
pending: "待审核",
disabled: "已停用",
};
return statusTextMap[status] ?? "未知";
}
function createStudentSummary(student) {
return {
id: student.id,
name: student.name,
level: getScoreLevel(student.score),
statusText: getStatusText(student.status),
};
}
function transformStudents(students, transformer) {
const result = [];
for (const student of students) {
result.push(transformer(student));
}
return result;
}
const students = [
{id: 1, name: "张三", score: 86, status: "active"},
{id: 2, name: "李四", score: 72, status: "pending"},
];
const summaries = transformStudents(students, createStudentSummary);
console.table(summaries);
console.log(students[0].level); // undefined,原记录没有被添加 level这段程序先用 transformStudents() 遍历,再把每条记录交给 createStudentSummary()。摘要函数继续调用成绩和状态函数,最终得到两条新摘要:张三为“良好/正常”,李四为“合格/待审核”。createStudentSummary() 返回新对象,没有给原学员记录添加等级字段。
练习与验收
- 使用“返回值”小节中的分级函数,分别调用
getLevel(59)、getLevel(60)和getLevel(90),应得到“需要补考”“合格”“优秀”。 - 将
greetStudent("张三")的调用结果保存到变量再输出。应先看到欢迎语,再看到undefined;说明输出日志与返回结果的区别。 - 给摘要案例添加一名成绩为
95、状态为disabled的学员,应多得到一条“优秀/已停用”的摘要,无需修改遍历函数。 - 说明为什么传入
createStudentSummary时不写括号,以及该函数最终在哪里被调用。 - 分别运行
formatStatus("archived")、calculateAverage(0, 0)和calculateAverage(Infinity, 2),结果应为“未知”、0和null;说明为什么后两种输入不能按同一规则处理。
下一节保留同样的“遍历 → 调用回调 → 收集结果”过程,用内置的 map()、filter() 表达它。
小结
- 函数通过参数接收输入,通过
return给出输出。 - 变量应声明在需要它的最小作用域,依赖优先通过参数传入。
let和const还受代码块范围限制,分支内声明的变量不能随意在分支外读取。 - 函数可以作为值传递,回调由另一个函数在合适时机调用。